witnora 0.13.3 → 0.13.5

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.
@@ -0,0 +1,359 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, readdir, rename } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { canonicalJson } from "./canonical.js";
5
+ export class FileActionCheckpointStore {
6
+ directory;
7
+ constructor(directory) { this.directory = resolve(directory); }
8
+ async load(actionId) {
9
+ try {
10
+ return parseCheckpoint(await readFile(this.path(actionId), "utf8"));
11
+ }
12
+ catch (error) {
13
+ if (isMissing(error))
14
+ return undefined;
15
+ throw error;
16
+ }
17
+ }
18
+ async save(checkpoint) {
19
+ const path = this.path(checkpoint.actionId);
20
+ await mkdir(dirname(path), { recursive: true });
21
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
22
+ const handle = await open(temporary, "w", 0o600);
23
+ try {
24
+ await handle.writeFile(`${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
25
+ await handle.sync();
26
+ }
27
+ finally {
28
+ await handle.close();
29
+ }
30
+ await rename(temporary, path);
31
+ }
32
+ async list() {
33
+ let names;
34
+ try {
35
+ names = await readdir(this.directory);
36
+ }
37
+ catch (error) {
38
+ if (isMissing(error))
39
+ return [];
40
+ throw error;
41
+ }
42
+ const values = await Promise.all(names.filter((name) => name.endsWith(".json")).map(async (name) => parseCheckpoint(await readFile(join(this.directory, name), "utf8"))));
43
+ return values.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
44
+ }
45
+ async claimExecution(actionId, ownerId, claimedAt) {
46
+ const path = this.claimPath(actionId);
47
+ await mkdir(dirname(path), { recursive: true });
48
+ const claimId = randomUUID();
49
+ try {
50
+ const handle = await open(path, "wx", 0o600);
51
+ try {
52
+ await handle.writeFile(`${JSON.stringify({ schemaVersion: "witnora.execution_claim.v0.1", actionId, claimId, ownerId, claimedAt })}\n`, "utf8");
53
+ await handle.sync();
54
+ }
55
+ finally {
56
+ await handle.close();
57
+ }
58
+ return { acquired: true, claimId };
59
+ }
60
+ catch (error) {
61
+ if (!isAlreadyExists(error))
62
+ throw error;
63
+ const existing = JSON.parse(await readFile(path, "utf8"));
64
+ if (typeof existing.claimId !== "string" || !existing.claimId)
65
+ throw new Error("Existing execution claim is invalid; refusing to dispatch.");
66
+ return { acquired: false, claimId: existing.claimId };
67
+ }
68
+ }
69
+ path(actionId) {
70
+ if (!/^[A-Za-z0-9._:-]+$/.test(actionId))
71
+ throw new Error("actionId contains unsupported characters.");
72
+ return join(this.directory, `${actionId}.json`);
73
+ }
74
+ claimPath(actionId) {
75
+ if (!/^[A-Za-z0-9._:-]+$/.test(actionId))
76
+ throw new Error("actionId contains unsupported characters.");
77
+ return join(this.directory, `${actionId}.execution-claim`);
78
+ }
79
+ }
80
+ export class DurableApprovedActionWorker {
81
+ options;
82
+ now;
83
+ chains = new Map();
84
+ timer;
85
+ lastHeartbeatAt;
86
+ lastError;
87
+ workerId = randomUUID();
88
+ constructor(options) {
89
+ this.options = options;
90
+ this.now = options.now ?? (() => new Date());
91
+ if (!options.config.adapterId || !validVersion(options.config.adapterVersion))
92
+ throw new Error("An exact adapter id and semantic version are required.");
93
+ if (!options.config.probeId || !options.config.probeCredentialHandle || options.probe.id !== options.config.probeId
94
+ || options.probe.credentialHandle !== options.config.probeCredentialHandle || options.probe.readOnly !== true)
95
+ throw new Error("An exact separate read-only probe credential is required.");
96
+ if (options.config.adapterId === options.config.probeId)
97
+ throw new Error("The adapter and outcome probe must be separate.");
98
+ if (options.runtime.reconcileReadOnly !== true)
99
+ throw new Error("Unknown writes require an explicitly read-only reconciliation path.");
100
+ this.lastHeartbeatAt = this.now().toISOString();
101
+ }
102
+ async track(input) {
103
+ assertProposal(input.proposal);
104
+ const existing = await this.options.store.load(input.actionId);
105
+ const digest = sha256(input.proposal);
106
+ if (existing) {
107
+ if (existing.proposalSha256 !== digest)
108
+ throw new Error("Action id was already tracked with a different proposal.");
109
+ return existing;
110
+ }
111
+ const checkpoint = {
112
+ schemaVersion: "witnora.durable_action_checkpoint.v0.1", actionId: input.actionId, phase: "TRACKED",
113
+ proposal: structuredClone(input.proposal), proposalSha256: digest, updatedAt: this.now().toISOString(),
114
+ };
115
+ await this.options.store.save(checkpoint);
116
+ return checkpoint;
117
+ }
118
+ tick(actionId) {
119
+ const previous = this.chains.get(actionId) ?? Promise.resolve(undefined);
120
+ const next = previous.catch(() => undefined).then(() => this.tickOnce(actionId));
121
+ this.chains.set(actionId, next);
122
+ return next.finally(() => { if (this.chains.get(actionId) === next)
123
+ this.chains.delete(actionId); });
124
+ }
125
+ start() {
126
+ if (this.timer)
127
+ return;
128
+ const poll = async () => {
129
+ this.lastHeartbeatAt = this.now().toISOString();
130
+ const checkpoints = await this.options.store.list();
131
+ let cycleError;
132
+ await Promise.all(checkpoints.filter((item) => !terminal(item.phase)).map((item) => this.tick(item.actionId).catch((error) => { cycleError = message(error); })));
133
+ this.lastError = cycleError;
134
+ };
135
+ void poll().catch((error) => { this.lastError = message(error); });
136
+ this.timer = setInterval(() => void poll().catch((error) => { this.lastError = message(error); }), this.options.config.pollIntervalMs ?? 2_000);
137
+ this.timer.unref();
138
+ }
139
+ stop() { if (this.timer)
140
+ clearInterval(this.timer); this.timer = undefined; }
141
+ async status() {
142
+ const checkpoints = await this.options.store.list();
143
+ return {
144
+ schemaVersion: "witnora.durable_action_worker_status.v0.1",
145
+ ready: Boolean(this.timer) && !this.lastError,
146
+ trackedActionCount: checkpoints.length,
147
+ pendingActionCount: checkpoints.filter((item) => !terminal(item.phase)).length,
148
+ unknownResultCount: checkpoints.filter((item) => item.phase === "UNKNOWN_RESULT").length,
149
+ lastHeartbeatAt: this.lastHeartbeatAt,
150
+ ...(this.lastError ? { lastError: this.lastError } : {}),
151
+ };
152
+ }
153
+ async tickOnce(actionId) {
154
+ let checkpoint = await this.options.store.load(actionId);
155
+ if (!checkpoint)
156
+ throw new Error(`Action ${actionId} is not tracked.`);
157
+ if (terminal(checkpoint.phase))
158
+ return checkpoint;
159
+ const action = await this.options.hosted.getAction(actionId);
160
+ if (!exactActionMatches(action, actionId, checkpoint.proposal, this.options.config.adapterId, this.options.config.adapterVersion)) {
161
+ return this.persist(checkpoint, "BLOCKED", { limitation: "Hosted action no longer matches the exact locally configured action, adapter, or build binding." });
162
+ }
163
+ if ((checkpoint.phase === "EXECUTION_STARTED" || checkpoint.phase === "UNKNOWN_RESULT") && checkpoint.grant) {
164
+ if (!exactGrantMatches(checkpoint.grant, actionId, checkpoint.proposal, this.options.config.runtimeIdentityId)) {
165
+ return this.persist(checkpoint, "BLOCKED", { limitation: "Hosted grant does not match the exact approved local action binding." });
166
+ }
167
+ const reconciled = await this.options.runtime.reconcile({ action, proposal: checkpoint.proposal, grant: checkpoint.grant });
168
+ if (!reconciled)
169
+ return this.persist(checkpoint, "UNKNOWN_RESULT", { limitation: "Write result is unknown. The worker will only use read-only reconciliation and will never redispatch this grant." });
170
+ checkpoint = await this.persist(checkpoint, "EXECUTED", { execution: reconciled, limitation: undefined });
171
+ }
172
+ if (!checkpoint.execution) {
173
+ if (action.status === "REJECTED" || action.status === "DENIED")
174
+ return this.persist(checkpoint, "DENIED");
175
+ if (action.status === "APPROVAL_EXPIRED" || action.status === "GRANT_EXPIRED" || (action.status === "APPROVED" && expired(action.approvalExpiresAt, this.now())))
176
+ return this.persist(checkpoint, "EXPIRED");
177
+ if (action.status === "PENDING_APPROVAL")
178
+ return this.persist(checkpoint, "WAITING_APPROVAL");
179
+ if (action.status !== "APPROVED" && action.status !== "ALLOWED") {
180
+ return this.persist(checkpoint, "BLOCKED", { limitation: `Action status ${action.status} is not executable.` });
181
+ }
182
+ }
183
+ if (!checkpoint.grant && !checkpoint.execution) {
184
+ const intent = checkpoint.proposal.executionIntent;
185
+ const grant = await this.options.hosted.issueExecutionGrant(actionId, {
186
+ runtimeIdentityId: this.options.config.runtimeIdentityId,
187
+ adapterId: intent.adapterId,
188
+ adapterVersionConstraint: intent.adapterVersionConstraint,
189
+ allowedOrigins: intent.allowedOrigins,
190
+ allowedOperation: intent.allowedOperation,
191
+ allowedResource: intent.allowedResource,
192
+ approvedParameters: intent.approvedParameters,
193
+ outcomePredicate: intent.outcomePredicate,
194
+ agentBuildId: intent.agentBuildId,
195
+ agentBuildDigest: intent.agentBuildDigest,
196
+ ttlSeconds: this.options.config.grantTtlSeconds ?? 120,
197
+ }, `durable-worker:grant:${actionId}`);
198
+ checkpoint = await this.persist(checkpoint, "GRANT_ISSUED", { grant });
199
+ }
200
+ const grant = checkpoint.grant;
201
+ if (!grant)
202
+ return this.persist(checkpoint, "BLOCKED", { limitation: "A durable execution result exists without its bound Hosted grant." });
203
+ const grantPayload = grant.grant.payload ?? {};
204
+ if (!exactGrantMatches(grant, actionId, checkpoint.proposal, this.options.config.runtimeIdentityId)) {
205
+ return this.persist(checkpoint, "BLOCKED", { limitation: "Hosted grant does not match the exact approved local action binding." });
206
+ }
207
+ if (!checkpoint.execution) {
208
+ if (grant.status !== "ISSUED" || expired(stringValue(grantPayload.expiresAt), this.now()))
209
+ return this.persist(checkpoint, "EXPIRED");
210
+ const claim = await this.options.store.claimExecution(actionId, this.workerId, this.now().toISOString());
211
+ if (!claim.acquired) {
212
+ checkpoint = await this.persist(checkpoint, "UNKNOWN_RESULT", {
213
+ executionClaimId: claim.claimId,
214
+ limitation: "Another Gateway process owns the durable execution claim. This worker will only reconcile and will never dispatch.",
215
+ });
216
+ const reconciled = await this.options.runtime.reconcile({ action, proposal: checkpoint.proposal, grant });
217
+ if (!reconciled)
218
+ return checkpoint;
219
+ checkpoint = await this.persist(checkpoint, "EXECUTED", { execution: reconciled, limitation: undefined });
220
+ }
221
+ else {
222
+ checkpoint = await this.persist(checkpoint, "EXECUTION_STARTED", { executionClaimId: claim.claimId });
223
+ }
224
+ }
225
+ if (!checkpoint.execution && checkpoint.phase === "EXECUTION_STARTED") {
226
+ if (!checkpoint.hostedReservation) {
227
+ const reservation = await this.options.runtime.prepareClaim({ action, proposal: checkpoint.proposal, grant });
228
+ if (!exactHostedReservation(reservation, actionId, grant.id))
229
+ return this.persist(checkpoint, "BLOCKED", { limitation: "Runtime claim is not bound to the exact action, grant, and execution session." });
230
+ const claimed = await this.options.hosted.claimExecutionGrant(grant.id, reservation.claim, `durable-worker:claim:${grant.id}`);
231
+ if (!claimed.acquired) {
232
+ checkpoint = await this.persist(checkpoint, "UNKNOWN_RESULT", {
233
+ limitation: "Hosted already has an execution reservation for this grant. This worker will only reconcile and will never dispatch.",
234
+ });
235
+ const reconciled = await this.options.runtime.reconcile({ action, proposal: checkpoint.proposal, grant });
236
+ if (!reconciled)
237
+ return checkpoint;
238
+ checkpoint = await this.persist(checkpoint, "EXECUTED", { execution: reconciled, limitation: undefined });
239
+ }
240
+ else {
241
+ checkpoint = await this.persist(checkpoint, "EXECUTION_STARTED", { hostedReservation: reservation });
242
+ }
243
+ }
244
+ }
245
+ if (!checkpoint.execution && checkpoint.phase === "EXECUTION_STARTED" && checkpoint.hostedReservation) {
246
+ try {
247
+ const execution = await this.options.runtime.execute({ action, proposal: checkpoint.proposal, grant, hostedReservation: checkpoint.hostedReservation });
248
+ checkpoint = await this.persist(checkpoint, "EXECUTED", { execution });
249
+ }
250
+ catch {
251
+ checkpoint = await this.persist(checkpoint, "UNKNOWN_RESULT", { limitation: "Write result is unknown. The worker will only use read-only reconciliation and will never redispatch this grant." });
252
+ const reconciled = await this.options.runtime.reconcile({ action, proposal: checkpoint.proposal, grant });
253
+ if (!reconciled)
254
+ return checkpoint;
255
+ checkpoint = await this.persist(checkpoint, "EXECUTED", { execution: reconciled, limitation: undefined });
256
+ }
257
+ }
258
+ if (!checkpoint.observation) {
259
+ const observation = await this.options.probe.observe({ action, proposal: checkpoint.proposal, grant, execution: checkpoint.execution });
260
+ if (!observation.observationSource || observation.observationSource !== this.options.config.probeId)
261
+ throw new Error("Outcome observation did not come from the exact configured probe.");
262
+ checkpoint = await this.persist(checkpoint, "PROBED", { observation });
263
+ }
264
+ const verified = await this.options.hosted.verifyAction(actionId, {
265
+ ...checkpoint.observation,
266
+ executionGrantId: grant.id,
267
+ executionSessionId: checkpoint.execution.executionSessionId,
268
+ }, `durable-worker:verify:${actionId}:${checkpoint.execution.executionSessionId}`);
269
+ if (verified.status !== "VERIFIED" || verified.verificationSuccess !== true)
270
+ throw new Error("Hosted outcome verification did not establish the expected result.");
271
+ const receipts = await this.options.hosted.listActionReceipts(actionId);
272
+ const signed = receipts.find((item) => Array.isArray(item.receipt?.signatureSet) && item.receipt.signatureSet.length > 0);
273
+ if (!signed)
274
+ throw new Error("Hosted did not automatically issue a signed action receipt.");
275
+ return this.persist(checkpoint, "COMPLETED", { receiptId: signed.id });
276
+ }
277
+ async persist(checkpoint, phase, patch = {}) {
278
+ const next = { ...checkpoint, ...patch, phase, updatedAt: this.now().toISOString() };
279
+ if (patch.limitation === undefined && "limitation" in patch)
280
+ delete next.limitation;
281
+ await this.options.store.save(next);
282
+ return next;
283
+ }
284
+ }
285
+ function exactActionMatches(action, actionId, proposal, adapterId, adapterVersion) {
286
+ const approved = action.assuranceContext?.executionIntent;
287
+ const proposed = proposal.executionIntent;
288
+ return action.id === actionId && action.externalId === proposal.externalId && action.actionType === proposal.actionType
289
+ && action.targetSystem === proposal.targetSystem && sha256(action.expectedState ?? {}) === sha256(proposal.expectedState ?? {})
290
+ && proposed.adapterId === adapterId && runtimeAdapterVersionSatisfies(adapterVersion, proposed.adapterVersionConstraint)
291
+ && approved?.adapterId === proposed.adapterId && approved.adapterVersionConstraint === proposed.adapterVersionConstraint
292
+ && canonicalJson(approved.allowedOrigins) === canonicalJson(proposed.allowedOrigins)
293
+ && approved.allowedOperation === proposed.allowedOperation && approved.allowedResource === proposed.allowedResource
294
+ && approved.approvedParametersDigest === sha256(proposed.approvedParameters)
295
+ && approved.outcomePredicateDigest === sha256(proposed.outcomePredicate)
296
+ && approved.agentBuildId === proposed.agentBuildId && approved.agentBuildDigest === proposed.agentBuildDigest;
297
+ }
298
+ function exactGrantMatches(grant, actionId, proposal, runtimeIdentityId) {
299
+ const payload = grant.grant.payload ?? {};
300
+ const intent = proposal.executionIntent;
301
+ return payload.executionGrantId === grant.id && payload.actionId === actionId && payload.adapterId === intent.adapterId
302
+ && payload.adapterVersionConstraint === intent.adapterVersionConstraint && payload.expectedRuntimeIdentityId === runtimeIdentityId
303
+ && canonicalJson(payload.allowedOrigins) === canonicalJson(intent.allowedOrigins)
304
+ && payload.allowedOperation === intent.allowedOperation && payload.allowedResource === intent.allowedResource
305
+ && payload.agentBuildId === intent.agentBuildId && payload.agentBuildDigest === intent.agentBuildDigest
306
+ && payload.parametersDigest === sha256(intent.approvedParameters) && payload.outcomePredicateDigest === sha256(intent.outcomePredicate);
307
+ }
308
+ function exactHostedReservation(reservation, actionId, executionGrantId) {
309
+ if (!reservation || typeof reservation !== "object" || typeof reservation.executionSessionId !== "string"
310
+ || !reservation.executionSessionId || !reservation.claim || typeof reservation.claim !== "object" || Array.isArray(reservation.claim))
311
+ return false;
312
+ const payload = reservation.claim.payload;
313
+ return Boolean(payload && typeof payload === "object" && !Array.isArray(payload)
314
+ && payload.actionId === actionId
315
+ && payload.executionGrantId === executionGrantId
316
+ && payload.executionSessionId === reservation.executionSessionId);
317
+ }
318
+ function parseCheckpoint(raw) {
319
+ const value = JSON.parse(raw);
320
+ if (value.schemaVersion !== "witnora.durable_action_checkpoint.v0.1" || !value.actionId || !value.phase || !value.proposal || !value.proposalSha256 || !value.updatedAt) {
321
+ throw new Error("Durable action checkpoint is invalid.");
322
+ }
323
+ return value;
324
+ }
325
+ function assertProposal(value) {
326
+ const intent = value?.executionIntent;
327
+ if (!value?.externalId || !value.actionType || !value.targetSystem || !intent?.adapterId || !intent.adapterVersionConstraint
328
+ || !Array.isArray(intent.allowedOrigins) || !intent.allowedOrigins.length || !intent.allowedOperation || !intent.allowedResource
329
+ || !intent.approvedParameters || typeof intent.approvedParameters !== "object" || !intent.outcomePredicate || typeof intent.outcomePredicate !== "object"
330
+ || !intent.agentBuildId || !/^[a-f0-9]{64}$/.test(intent.agentBuildDigest)) {
331
+ throw new Error("Tracked action is missing an exact execution intent.");
332
+ }
333
+ }
334
+ function sha256(value) { return createHash("sha256").update(canonicalJson(value)).digest("hex"); }
335
+ function expired(value, now) { return !value || !Number.isFinite(Date.parse(value)) || Date.parse(value) <= now.getTime(); }
336
+ function stringValue(value) { return typeof value === "string" ? value : undefined; }
337
+ function validVersion(value) { return /^v?\d+\.\d+\.\d+$/.test(value); }
338
+ export function runtimeAdapterVersionSatisfies(version, constraint) {
339
+ const parsed = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version);
340
+ const exact = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(constraint);
341
+ if (!parsed)
342
+ return false;
343
+ if (exact)
344
+ return parsed.slice(1).join(".") === exact.slice(1).join(".");
345
+ const compatible = /^\^v?(\d+)\.(\d+)\.(\d+)$/.exec(constraint);
346
+ if (!compatible)
347
+ return false;
348
+ const [major, minor, patch] = parsed.slice(1).map(Number);
349
+ const [requiredMajor, requiredMinor, requiredPatch] = compatible.slice(1).map(Number);
350
+ if (requiredMajor > 0)
351
+ return major === requiredMajor && (minor > requiredMinor || (minor === requiredMinor && patch >= requiredPatch));
352
+ if (requiredMinor > 0)
353
+ return major === 0 && minor === requiredMinor && patch >= requiredPatch;
354
+ return major === 0 && minor === 0 && patch === requiredPatch;
355
+ }
356
+ function terminal(phase) { return phase === "DENIED" || phase === "EXPIRED" || phase === "BLOCKED" || phase === "COMPLETED"; }
357
+ function isMissing(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
358
+ function isAlreadyExists(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST"); }
359
+ function message(error) { return error instanceof Error ? error.message : String(error); }
@@ -38,6 +38,25 @@ export interface RemoteCollectorAck {
38
38
  alerts: Array<Record<string, unknown>>;
39
39
  run: Record<string, unknown>;
40
40
  }
41
+ export interface RuntimeReadinessBinding {
42
+ collectorId: string;
43
+ sourceKeyId: string;
44
+ configDigestSha256: string;
45
+ runtimeIdentity: {
46
+ id: string;
47
+ };
48
+ mandateDigestSha256: string;
49
+ adapterDigestSha256: string;
50
+ probeDigestSha256: string;
51
+ fixtureContractDigestSha256: string;
52
+ }
53
+ export interface CollectorRuntimeHeartbeat {
54
+ classification: "LOCAL_SANDBOX_ONLY";
55
+ binding: RuntimeReadinessBinding;
56
+ workerReady: boolean;
57
+ fixtureReady: boolean;
58
+ lastError?: string;
59
+ }
41
60
  export interface RemoteCollectorClientOptions {
42
61
  baseUrl: string;
43
62
  projectId: string;
@@ -85,6 +104,7 @@ export declare class RemoteCollectorClient {
85
104
  pendingRecordCount: number;
86
105
  lastAckSequence?: number;
87
106
  occurredAt?: string;
107
+ runtime?: CollectorRuntimeHeartbeat;
88
108
  }): Promise<Record<string, unknown>>;
89
109
  reconcile(runId: string, receipt: Record<string, unknown>): Promise<Record<string, unknown>>;
90
110
  proposeAction(proposal: Record<string, unknown>, idempotencyKey: string): Promise<Record<string, unknown>>;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-collector.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/remote-collector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-collector.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/remote-collector.ts"],"names":[],"mappings":"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"}
@@ -104,6 +104,7 @@ export class RemoteCollectorClient {
104
104
  occurredAt: input.occurredAt ?? new Date().toISOString(),
105
105
  pendingRecordCount: input.pendingRecordCount,
106
106
  lastAckSequence: input.lastAckSequence,
107
+ runtime: input.runtime,
107
108
  };
108
109
  const payloadSha256 = sha256(canonicalJson(payload));
109
110
  return this.json("collector-heartbeats", {
package/dist/onboard.js CHANGED
@@ -7,7 +7,9 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
7
7
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
8
8
  import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
9
9
  import { writeTryEvidence } from "./try.js";
10
- import { doctorCustomerGateway, initializeCustomerGateway, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
10
+ import { doctorCustomerGateway, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
11
+ import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
12
+ import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
11
13
  export async function runOnboard(options) {
12
14
  const requestFetch = options.fetch ?? fetch;
13
15
  const output = options.output ?? ((message) => process.stdout.write(message));
@@ -36,6 +38,44 @@ export async function runOnboard(options) {
36
38
  if (!setupPlan || !setupPlan.authorizedOperations.some((item) => item.kind === "authorize_install")) {
37
39
  throw new Error("Confirm the Setup Plan and authorize installation in the Witnora Workspace before running onboard.");
38
40
  }
41
+ let localRuntime = await discoverRuntimeReferences({
42
+ repository: repositoryPath, configHome: options.configHome, projectId: token.projectId, server, env: options.env ?? process.env,
43
+ });
44
+ let runtimeLimitation = localRuntime.limitation;
45
+ let runtimeBootstrapRequired = !localRuntime.references;
46
+ if (localRuntime.references?.probeApiKeyId) {
47
+ try {
48
+ if (!await automaticRuntimeReferencesUsable(localRuntime.references))
49
+ throw new Error("local bootstrap material invalid");
50
+ await verifyAutomaticRuntimeProbe({ references: localRuntime.references, projectId: token.projectId, server, fetch: requestFetch });
51
+ }
52
+ catch {
53
+ runtimeBootstrapRequired = true;
54
+ }
55
+ }
56
+ if (runtimeBootstrapRequired) {
57
+ try {
58
+ const references = localRuntime.references;
59
+ const modules = references?.sandboxOrigin && references.adapterDigestSha256 && references.probeDigestSha256 && references.fixtureContractDigestSha256
60
+ ? { sandboxOrigin: references.sandboxOrigin, adapterDigestSha256: references.adapterDigestSha256, probeDigestSha256: references.probeDigestSha256, fixtureContractDigestSha256: references.fixtureContractDigestSha256 }
61
+ : await planRuntimeSandboxModules();
62
+ const bootstrap = await bootstrapLocalRuntime({
63
+ projectId: token.projectId,
64
+ planId: setupPlan.id,
65
+ server,
66
+ apiKey: token.apiKey,
67
+ configHome: options.configHome,
68
+ fetch: requestFetch,
69
+ modules,
70
+ });
71
+ localRuntime = { references: bootstrap.references };
72
+ runtimeLimitation = undefined;
73
+ }
74
+ catch (error) {
75
+ const detail = error instanceof Error ? error.message : "Unknown Runtime bootstrap error.";
76
+ runtimeLimitation = `Hosted Runtime bootstrap did not complete: ${detail.slice(0, 500)} The existing Gateway was preserved and setup remains RECORDED_ONLY.`;
77
+ }
78
+ }
39
79
  const attemptId = `install-${Date.now()}-${randomSuffix()}`;
40
80
  const generatedFiles = [];
41
81
  const gatewayLifecycle = options.gatewayLifecycle ?? {
@@ -44,6 +84,7 @@ export async function runOnboard(options) {
44
84
  };
45
85
  let gatewayMigration;
46
86
  let managedGateway;
87
+ let runtimeUpgrade;
47
88
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
48
89
  try {
49
90
  generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
@@ -84,17 +125,49 @@ export async function runOnboard(options) {
84
125
  throw new Error(`Existing Gateway setup is incomplete. Preserve the existing files and repair or remove the partial setup in Advanced mode. Missing: ${gatewayState.missing.map((path) => basename(path)).join(", ")}.`);
85
126
  }
86
127
  if (gatewayState.status === "absent") {
87
- const gateway = await initializeCustomerGateway({
88
- projectId: token.projectId, server, repository: repositoryPath, authorization: token,
89
- fetch: requestFetch, configHome: options.configHome, output,
90
- });
128
+ let gateway;
129
+ try {
130
+ gateway = await initializeCustomerGateway({
131
+ projectId: token.projectId, server, repository: repositoryPath, authorization: token,
132
+ runtimeReferences: localRuntime.references,
133
+ fetch: requestFetch, configHome: options.configHome, output,
134
+ });
135
+ }
136
+ catch (error) {
137
+ if (!(error instanceof RuntimeSetupNotReadyError) || !localRuntime.references)
138
+ throw error;
139
+ runtimeLimitation = `Local sandbox Runtime references were found but did not establish readiness: ${error.message}`;
140
+ gateway = await initializeCustomerGateway({ projectId: token.projectId, server, repository: repositoryPath, authorization: token, fetch: requestFetch, configHome: options.configHome, output });
141
+ }
91
142
  generatedFiles.push(...gateway.generatedFiles);
92
143
  }
93
144
  else {
94
145
  const binding = await inspectGatewayBinding(gatewayState.directory, token.projectId, server);
95
146
  if (binding.matches) {
96
147
  await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
97
- output("\nExisting customer-owned Gateway matches this project; verifying and reusing it.\n");
148
+ if (localRuntime.references) {
149
+ try {
150
+ runtimeUpgrade = await upgradeCustomerGatewayRuntime({ repository: repositoryPath, authorization: token, runtimeReferences: localRuntime.references, configHome: options.configHome, fetch: requestFetch });
151
+ if (runtimeUpgrade.changed) {
152
+ const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
153
+ if (stopped.state !== "STOPPED" || stopped.healthy)
154
+ throw new Error("The prior managed Gateway process did not stop before Runtime generation replacement.");
155
+ }
156
+ runtimeLimitation = undefined;
157
+ output(runtimeUpgrade.changed
158
+ ? "\nExisting customer-owned Gateway matches this project; safely replaced its generated Runtime generation, stopped the prior process, and preserved a local backup.\n"
159
+ : "\nExisting customer-owned Gateway already has the exact current Runtime generation; verifying and reusing it.\n");
160
+ }
161
+ catch (error) {
162
+ if (!(error instanceof RuntimeSetupNotReadyError))
163
+ throw error;
164
+ runtimeLimitation = `Local sandbox Runtime references were found but did not establish readiness: ${error.message}`;
165
+ output("\nExisting customer-owned Gateway remains RECORDED_ONLY because its Runtime references did not pass readiness checks.\n");
166
+ }
167
+ }
168
+ else {
169
+ output("\nExisting customer-owned Gateway matches this project; verifying and reusing it.\n");
170
+ }
98
171
  }
99
172
  else {
100
173
  await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
@@ -102,12 +175,14 @@ export async function runOnboard(options) {
102
175
  output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
103
176
  const gateway = await initializeCustomerGateway({
104
177
  projectId: token.projectId, server, repository: repositoryPath, authorization: token,
178
+ runtimeReferences: localRuntime.references,
105
179
  fetch: requestFetch, configHome: options.configHome, output,
106
180
  });
107
181
  generatedFiles.push(...gateway.generatedFiles);
108
182
  }
109
183
  }
110
- generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
184
+ const runtimeConfigured = await gatewayHasRuntimeWorker(repositoryPath);
185
+ generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name, runtimeConfigured));
111
186
  managedGateway = await gatewayLifecycle.start({
112
187
  repository: repositoryPath,
113
188
  configHome: options.configHome,
@@ -117,10 +192,14 @@ export async function runOnboard(options) {
117
192
  if (!managedGateway.healthy)
118
193
  throw new Error(managedGateway.detail);
119
194
  const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
120
- if (doctor.overall !== "READY_TO_RECORD")
195
+ if (doctor.overall !== "READY_TO_RECORD" && doctor.overall !== "READY_FOR_RUNTIME")
121
196
  throw new Error(doctor.checks.filter((check) => check.status === "FAIL").map((check) => check.message).join(" "));
197
+ const runtimeBinding = doctor.overall === "READY_FOR_RUNTIME" ? await inspectLocalRuntimeBinding({ repository: repositoryPath }).catch(() => undefined) : undefined;
198
+ const runtimeReadiness = doctor.overall === "READY_FOR_RUNTIME" && runtimeBinding
199
+ ? { state: "LOCAL_SANDBOX_READY", checkedAt: new Date().toISOString(), limitations: [] }
200
+ : { state: "RECORDED_ONLY", checkedAt: new Date().toISOString(), limitations: [runtimeLimitation ?? "The local sandbox Runtime worker has not established exact adapter, probe, source-key, identity, mandate, and readiness bindings."] };
122
201
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
123
- status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles),
202
+ status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), runtimeReadiness, ...(runtimeBinding ? { runtimeBinding } : {}),
124
203
  });
125
204
  output(`\nWitnora Setup Autopilot completed for ${repository.name}.\n`);
126
205
  output(`Project: ${token.projectId}\nTemplate: ${repository.template} (${repository.kind})\n`);
@@ -128,11 +207,14 @@ export async function runOnboard(options) {
128
207
  output(`Private discovery: ${discovery.capabilityCount} capability group(s); ${discovery.unknownCapabilityCount} pending confirmation.\n`);
129
208
  output("Installed: customer-owned Gateway, default-deny policy, independent-probe contract, review contract, and PR/release/nightly CI.\n");
130
209
  output(`Gateway: ${managedGateway.state} at ${managedGateway.baseUrl}${managedGateway.pid ? ` (process ${managedGateway.pid})` : ""}.\n`);
210
+ output(runtimeReadiness.state === "LOCAL_SANDBOX_READY"
211
+ ? "Runtime: LOCAL_SANDBOX_READY. This proves only the generated localhost sandbox loop is ready; it does not establish coverage, CURRENT, or a verified customer outcome.\n"
212
+ : `Runtime: RECORDED_ONLY. ${runtimeReadiness.limitations[0]}\n`);
131
213
  output("The self-test remains isolated. The Gateway is ready in the background; run the agent normally. The first source-signed, server-reconciled Gateway run completes onboarding.\n");
132
214
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
133
215
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
134
216
  gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
135
- gateway: managedGateway };
217
+ gateway: managedGateway, runtimeReadiness };
136
218
  }
137
219
  catch (error) {
138
220
  const diagnosis = error instanceof Error ? error.message : String(error);
@@ -140,6 +222,7 @@ export async function runOnboard(options) {
140
222
  await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch }).catch(() => undefined);
141
223
  }
142
224
  await rollbackGeneratedFiles(generatedFiles);
225
+ await runtimeUpgrade?.rollback().catch(() => undefined);
143
226
  if (gatewayMigration)
144
227
  await restoreGateway(gatewayMigration);
145
228
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
@@ -199,10 +282,10 @@ async function restoreGateway(migration) {
199
282
  function safePathSegment(value) {
200
283
  return (value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-|-$/g, "") || "unknown-project").slice(0, 80);
201
284
  }
202
- async function generateAutopilotFiles(repositoryPath, subject) {
285
+ async function generateAutopilotFiles(repositoryPath, subject, runtimeConfigured) {
203
286
  const files = new Map([
204
287
  [".witnora/setup/policy.json", `${JSON.stringify({ schemaVersion: "witnora.setup_policy.v0.1", subject, unknownCapabilities: "pending_confirmation", defaultDecision: "deny", environment: "sandbox" }, null, 2)}\n`],
205
- [".witnora/setup/outcome-probe.json", `${JSON.stringify({ schemaVersion: "witnora.outcome_probe_contract.v0.1", mode: "independent_read_only", credentialEnv: "WITNORA_OUTCOME_PROBE_CREDENTIAL", status: "awaiting_customer_secret", uploadsCredential: false }, null, 2)}\n`],
288
+ [".witnora/setup/outcome-probe.json", `${JSON.stringify({ schemaVersion: "witnora.outcome_probe_contract.v0.1", mode: "independent_read_only", status: runtimeConfigured ? "configured" : "awaiting_runtime_bootstrap", uploadsCredential: false }, null, 2)}\n`],
206
289
  [".witnora/setup/review.json", `${JSON.stringify({ schemaVersion: "witnora.review_contract.v0.1", independentReviewRequired: true, syntheticEvidenceEligible: false }, null, 2)}\n`],
207
290
  [".github/workflows/witnora-assurance.yml", continuousAssuranceWorkflow()],
208
291
  ]);
@@ -217,6 +300,10 @@ async function generateAutopilotFiles(repositoryPath, subject) {
217
300
  }
218
301
  return written;
219
302
  }
303
+ async function gatewayHasRuntimeWorker(repositoryPath) {
304
+ const config = await optionalJson(join(repositoryPath, ".witnora", "gateway", "gateway.json"));
305
+ return Boolean(config?.runtimeWorker && typeof config.runtimeWorker === "object" && !Array.isArray(config.runtimeWorker));
306
+ }
220
307
  function continuousAssuranceWorkflow() {
221
308
  return `name: Witnora assurance\non:\n pull_request:\n push:\n tags: [\"v*\"]\n schedule:\n - cron: \"17 3 * * *\"\n workflow_dispatch:\npermissions:\n contents: read\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npx witnora@latest release-gate --config witnora.config.json --strict\n`;
222
309
  }