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.
- package/dist/gateway.js +686 -29
- package/dist/internal/control-client/collector-gateway.d.ts +18 -1
- package/dist/internal/control-client/collector-gateway.d.ts.map +1 -1
- package/dist/internal/control-client/collector-gateway.js +34 -3
- package/dist/internal/control-client/durable-action-worker.d.ts +194 -0
- package/dist/internal/control-client/durable-action-worker.d.ts.map +1 -0
- package/dist/internal/control-client/durable-action-worker.js +359 -0
- package/dist/internal/control-client/remote-collector.d.ts +20 -0
- package/dist/internal/control-client/remote-collector.d.ts.map +1 -1
- package/dist/internal/control-client/remote-collector.js +1 -0
- package/dist/onboard.js +99 -12
- package/dist/probe-process.js +135 -0
- package/dist/runtime-bootstrap.js +333 -0
- package/dist/runtime-sandbox-fixture.js +166 -0
- package/dist/runtime-sandbox-kit.js +295 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
}
|