witnora 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/conformance.js +1 -1
- package/dist/control-plane.js +4 -4
- package/dist/gateway.js +221 -15
- package/dist/internal/control-client/canonical.d.ts +2 -0
- package/dist/internal/control-client/canonical.d.ts.map +1 -0
- package/dist/internal/control-client/canonical.js +19 -0
- package/dist/internal/control-client/collector-gateway.d.ts +64 -0
- package/dist/internal/control-client/collector-gateway.d.ts.map +1 -0
- package/dist/internal/control-client/collector-gateway.js +392 -0
- 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 +132 -0
- package/dist/internal/control-client/remote-collector.d.ts.map +1 -0
- package/dist/internal/control-client/remote-collector.js +318 -0
- package/dist/onboarding-templates.js +2 -2
- package/dist/probe-process.js +135 -0
- package/package.json +3 -6
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.d.ts +0 -0
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.d.ts.map +0 -0
- /package/dist/{vendor/agentcert-sdk → internal/control-client}/deployment-enforcement.js +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { createHash, createPublicKey, sign, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { canonicalJson } from "./canonical.js";
|
|
6
|
+
import { DurableRemoteCollectorQueue, RemoteCollectorApiError, } from "./remote-collector.js";
|
|
7
|
+
export async function startCustomerOwnedCollectorGateway(options) {
|
|
8
|
+
if (options.gatewayToken.length < 24)
|
|
9
|
+
throw new Error("gatewayToken must contain at least 24 characters.");
|
|
10
|
+
const storageDirectory = resolve(options.storageDirectory);
|
|
11
|
+
await mkdir(storageDirectory, { recursive: true });
|
|
12
|
+
const signer = options.keyRing.activeSigner();
|
|
13
|
+
let sourceKeyRegistered = false;
|
|
14
|
+
const journals = new Map();
|
|
15
|
+
const activeCollector = {
|
|
16
|
+
id: options.keyRing.collectorId,
|
|
17
|
+
version: options.collectorVersion ?? "0.2.0",
|
|
18
|
+
environment: options.environment ?? "customer-owned",
|
|
19
|
+
keyId: signer.keyId,
|
|
20
|
+
publicKeySha256: publicKeyFingerprint(signer.publicKeyPem),
|
|
21
|
+
};
|
|
22
|
+
let lastRemoteSuccessAt;
|
|
23
|
+
let lastRemoteError;
|
|
24
|
+
const ensureSourceKeyRegistered = async () => {
|
|
25
|
+
if (sourceKeyRegistered)
|
|
26
|
+
return;
|
|
27
|
+
await options.client.registerSourceKey(options.keyRing.registration());
|
|
28
|
+
sourceKeyRegistered = true;
|
|
29
|
+
};
|
|
30
|
+
const journalFor = async (runId) => {
|
|
31
|
+
identifier(runId, "runId");
|
|
32
|
+
const existing = journals.get(runId);
|
|
33
|
+
if (existing)
|
|
34
|
+
return existing;
|
|
35
|
+
const queue = new DurableRemoteCollectorQueue(storageDirectory, runId);
|
|
36
|
+
const first = (await queue.all())[0];
|
|
37
|
+
const runSigner = first ? options.keyRing.signerFor(first.collector.keyId) : signer;
|
|
38
|
+
const journal = await GatewayRunJournal.open(storageDirectory, runId, first?.collector ?? activeCollector, runSigner);
|
|
39
|
+
journals.set(runId, journal);
|
|
40
|
+
return journal;
|
|
41
|
+
};
|
|
42
|
+
const flush = async () => {
|
|
43
|
+
let delivered = 0;
|
|
44
|
+
let reconciled = 0;
|
|
45
|
+
let pending = 0;
|
|
46
|
+
try {
|
|
47
|
+
await ensureSourceKeyRegistered();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastRemoteError = message(error);
|
|
51
|
+
}
|
|
52
|
+
for (const journal of journals.values()) {
|
|
53
|
+
try {
|
|
54
|
+
if (!sourceKeyRegistered)
|
|
55
|
+
throw new Error(lastRemoteError ?? "Collector source key is not registered.");
|
|
56
|
+
delivered += (await journal.queue.replay(options.client)).delivered;
|
|
57
|
+
if (await journal.reconcileIfReady(options.client))
|
|
58
|
+
reconciled += 1;
|
|
59
|
+
lastRemoteSuccessAt = new Date().toISOString();
|
|
60
|
+
lastRemoteError = undefined;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
lastRemoteError = message(error);
|
|
64
|
+
}
|
|
65
|
+
pending += (await journal.queue.pending()).length;
|
|
66
|
+
}
|
|
67
|
+
return { delivered, reconciled, pending };
|
|
68
|
+
};
|
|
69
|
+
const status = async () => {
|
|
70
|
+
let pendingRecordCount = 0;
|
|
71
|
+
let lastAckSequence;
|
|
72
|
+
for (const journal of journals.values()) {
|
|
73
|
+
pendingRecordCount += (await journal.queue.pending()).length;
|
|
74
|
+
const ack = await journal.queue.currentAck();
|
|
75
|
+
if (ack.sequence >= 0)
|
|
76
|
+
lastAckSequence = Math.max(lastAckSequence ?? -1, ack.sequence);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
schemaVersion: "agentcert.customer_collector_gateway_status.v0.2",
|
|
80
|
+
collectorId: activeCollector.id,
|
|
81
|
+
sourceKeyId: activeCollector.keyId,
|
|
82
|
+
runCount: journals.size,
|
|
83
|
+
pendingRecordCount,
|
|
84
|
+
lastAckSequence,
|
|
85
|
+
lastRemoteSuccessAt,
|
|
86
|
+
lastRemoteError,
|
|
87
|
+
...(options.actionWorker ? { actionWorker: await options.actionWorker.status() } : {}),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const server = createServer(async (request, response) => {
|
|
91
|
+
try {
|
|
92
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
93
|
+
if (request.method === "GET" && url.pathname === "/healthz")
|
|
94
|
+
return json(response, 200, await status());
|
|
95
|
+
authenticate(request, options.gatewayToken);
|
|
96
|
+
if (request.method === "POST" && url.pathname === "/v1/flush")
|
|
97
|
+
return json(response, 200, await flush());
|
|
98
|
+
if (request.method === "POST" && url.pathname === "/v1/actions") {
|
|
99
|
+
const body = await readJson(request, options.maxBodyBytes ?? 1_048_576);
|
|
100
|
+
const proposal = object(body.proposal);
|
|
101
|
+
const action = await options.client.proposeAction(proposal, identifier(body.idempotencyKey, "idempotencyKey"));
|
|
102
|
+
if (options.actionWorker)
|
|
103
|
+
await options.actionWorker.track({ actionId: identifier(action.id, "actionId"), proposal });
|
|
104
|
+
return json(response, 202, action);
|
|
105
|
+
}
|
|
106
|
+
const actionRoute = url.pathname.match(/^\/v1\/actions\/([A-Za-z0-9._:-]+)$/);
|
|
107
|
+
if (request.method === "GET" && actionRoute) {
|
|
108
|
+
return json(response, 200, await options.client.getAction(identifier(actionRoute[1], "actionId")));
|
|
109
|
+
}
|
|
110
|
+
const grantRoute = url.pathname.match(/^\/v1\/actions\/([A-Za-z0-9._:-]+)\/execution-grant$/);
|
|
111
|
+
if (request.method === "POST" && grantRoute) {
|
|
112
|
+
const actionId = identifier(grantRoute[1], "actionId");
|
|
113
|
+
const action = await options.client.getAction(actionId);
|
|
114
|
+
if (action.status !== "APPROVED" && action.status !== "ALLOWED") {
|
|
115
|
+
throw new GatewayRequestError(409, `Action ${actionId} is not approved for execution.`);
|
|
116
|
+
}
|
|
117
|
+
const body = await readJson(request, options.maxBodyBytes ?? 1_048_576);
|
|
118
|
+
return json(response, 201, await options.client.issueExecutionGrant(actionId, object(body.grant), identifier(body.idempotencyKey, "idempotencyKey")));
|
|
119
|
+
}
|
|
120
|
+
const route = url.pathname.match(/^\/v1\/runs\/([A-Za-z0-9._:-]+)\/(start|events|drops|complete)$/);
|
|
121
|
+
if (!route)
|
|
122
|
+
return json(response, 404, { error: "not found" });
|
|
123
|
+
const runId = route[1];
|
|
124
|
+
const operation = route[2];
|
|
125
|
+
const body = await readJson(request, options.maxBodyBytes ?? 1_048_576);
|
|
126
|
+
const journal = await journalFor(runId);
|
|
127
|
+
let record;
|
|
128
|
+
if (operation === "start") {
|
|
129
|
+
record = await journal.append("RUN_STARTED", object(body.payload), String(body.idempotencyKey ?? "run-start"));
|
|
130
|
+
}
|
|
131
|
+
else if (operation === "events") {
|
|
132
|
+
record = await journal.append(identifier(body.type, "type"), object(body.payload), identifier(body.idempotencyKey, "idempotencyKey"));
|
|
133
|
+
}
|
|
134
|
+
else if (operation === "drops") {
|
|
135
|
+
const count = positiveInteger(body.count, "count");
|
|
136
|
+
record = await journal.append("EVENTS_DROPPED", { count, reason: required(body.reason, "reason") }, identifier(body.idempotencyKey, "idempotencyKey"), count);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const input = body;
|
|
140
|
+
record = await journal.append("RUN_COMPLETED", object(input.payload), String(input.idempotencyKey ?? "run-complete"));
|
|
141
|
+
await journal.writeReceipt({
|
|
142
|
+
mandateDigests: strings(input.mandateDigests),
|
|
143
|
+
actionIds: strings(input.actionIds),
|
|
144
|
+
evidenceStrength: object(input.evidenceStrength),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const remote = await flush();
|
|
148
|
+
return json(response, 202, { durable: true, record, remote });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error instanceof RemoteCollectorApiError) {
|
|
152
|
+
const status = Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 502;
|
|
153
|
+
return json(response, status, status === error.status ? {
|
|
154
|
+
error: error.message,
|
|
155
|
+
code: error.code,
|
|
156
|
+
...(error.recovery ? { recovery: error.recovery } : {}),
|
|
157
|
+
} : { error: "Hosted API returned an invalid error response.", code: "remote_collector_invalid_status" });
|
|
158
|
+
}
|
|
159
|
+
const statusCode = error instanceof GatewayRequestError ? error.status : 500;
|
|
160
|
+
return json(response, statusCode, { error: message(error) });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
await listen(server, options.port ?? 0, options.host ?? "127.0.0.1");
|
|
164
|
+
const address = server.address();
|
|
165
|
+
if (!address || typeof address === "string")
|
|
166
|
+
throw new Error("Collector gateway did not bind a TCP port.");
|
|
167
|
+
const flushTimer = setInterval(() => void flush(), options.flushIntervalMs ?? 5_000);
|
|
168
|
+
const heartbeatTimer = setInterval(async () => {
|
|
169
|
+
const current = await status();
|
|
170
|
+
try {
|
|
171
|
+
await ensureSourceKeyRegistered();
|
|
172
|
+
await options.client.heartbeat({
|
|
173
|
+
collectorId: activeCollector.id,
|
|
174
|
+
signer,
|
|
175
|
+
pendingRecordCount: current.pendingRecordCount,
|
|
176
|
+
lastAckSequence: current.lastAckSequence,
|
|
177
|
+
});
|
|
178
|
+
lastRemoteSuccessAt = new Date().toISOString();
|
|
179
|
+
lastRemoteError = undefined;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
lastRemoteError = message(error);
|
|
183
|
+
}
|
|
184
|
+
}, options.heartbeatIntervalMs ?? 30_000);
|
|
185
|
+
flushTimer.unref();
|
|
186
|
+
heartbeatTimer.unref();
|
|
187
|
+
for (const runId of await discoverRunIds(storageDirectory))
|
|
188
|
+
await journalFor(runId);
|
|
189
|
+
await flush();
|
|
190
|
+
return {
|
|
191
|
+
baseUrl: `http://${options.host ?? "127.0.0.1"}:${address.port}`,
|
|
192
|
+
close: async () => {
|
|
193
|
+
clearInterval(flushTimer);
|
|
194
|
+
clearInterval(heartbeatTimer);
|
|
195
|
+
await options.actionWorker?.close?.();
|
|
196
|
+
await flush();
|
|
197
|
+
await close(server);
|
|
198
|
+
},
|
|
199
|
+
flush,
|
|
200
|
+
status,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
class GatewayRunJournal {
|
|
204
|
+
runId;
|
|
205
|
+
collector;
|
|
206
|
+
signer;
|
|
207
|
+
queue;
|
|
208
|
+
records;
|
|
209
|
+
chain = Promise.resolve();
|
|
210
|
+
receiptPath;
|
|
211
|
+
reconciliationPath;
|
|
212
|
+
constructor(directory, runId, collector, signer, records) {
|
|
213
|
+
this.runId = runId;
|
|
214
|
+
this.collector = collector;
|
|
215
|
+
this.signer = signer;
|
|
216
|
+
this.queue = new DurableRemoteCollectorQueue(directory, runId);
|
|
217
|
+
this.records = records;
|
|
218
|
+
const base = safeFileName(runId);
|
|
219
|
+
this.receiptPath = join(resolve(directory), `${base}.receipt.json`);
|
|
220
|
+
this.reconciliationPath = join(resolve(directory), `${base}.reconciled.json`);
|
|
221
|
+
}
|
|
222
|
+
static async open(directory, runId, collector, signer) {
|
|
223
|
+
const queue = new DurableRemoteCollectorQueue(directory, runId);
|
|
224
|
+
const records = await queue.all();
|
|
225
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
226
|
+
const record = records[index];
|
|
227
|
+
if (record.runId !== runId || canonicalJson(record.collector) !== canonicalJson(collector))
|
|
228
|
+
throw new Error(`Stored gateway journal for ${runId} has a different collector identity.`);
|
|
229
|
+
if (index > 0 && record.previousEventHash !== records[index - 1].eventHash)
|
|
230
|
+
throw new Error(`Stored gateway journal for ${runId} has a broken hash chain.`);
|
|
231
|
+
}
|
|
232
|
+
return new GatewayRunJournal(directory, runId, collector, signer, records);
|
|
233
|
+
}
|
|
234
|
+
async append(type, payload, idempotencyKey, skippedSequences = 0) {
|
|
235
|
+
identifier(idempotencyKey, "idempotencyKey");
|
|
236
|
+
let result;
|
|
237
|
+
const operation = this.chain.then(async () => {
|
|
238
|
+
const recordId = sha256(`${this.runId}:${idempotencyKey}`);
|
|
239
|
+
const replay = this.records.find((record) => record.recordId === recordId);
|
|
240
|
+
if (replay) {
|
|
241
|
+
if (replay.type !== type || canonicalJson(replay.payload) !== canonicalJson(payload))
|
|
242
|
+
throw new GatewayRequestError(409, `idempotencyKey ${idempotencyKey} was already used with different content.`);
|
|
243
|
+
result = replay;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (this.records.at(-1)?.type === "RUN_COMPLETED")
|
|
247
|
+
throw new GatewayRequestError(409, `Run ${this.runId} is already complete.`);
|
|
248
|
+
if (this.records.length === 0 && type !== "RUN_STARTED")
|
|
249
|
+
throw new GatewayRequestError(409, `Run ${this.runId} must start before appending records.`);
|
|
250
|
+
const previous = this.records.at(-1);
|
|
251
|
+
const occurredAt = new Date().toISOString();
|
|
252
|
+
const payloadSha256 = sha256(canonicalJson(payload));
|
|
253
|
+
const unsigned = {
|
|
254
|
+
schemaVersion: "agentcert.trusted_action_record.v0.1",
|
|
255
|
+
recordId,
|
|
256
|
+
runId: this.runId,
|
|
257
|
+
sequence: (previous?.sequence ?? -1) + 1 + skippedSequences,
|
|
258
|
+
occurredAt,
|
|
259
|
+
type,
|
|
260
|
+
collector: this.collector,
|
|
261
|
+
previousEventHash: previous?.eventHash,
|
|
262
|
+
payload,
|
|
263
|
+
payloadSha256,
|
|
264
|
+
};
|
|
265
|
+
const eventHash = sha256(canonicalJson(unsigned));
|
|
266
|
+
result = { ...unsigned, eventHash, sourceSignature: signDigest(eventHash, this.signer) };
|
|
267
|
+
await this.queue.enqueue(result);
|
|
268
|
+
this.records.push(result);
|
|
269
|
+
});
|
|
270
|
+
this.chain = operation.catch(() => undefined);
|
|
271
|
+
await operation;
|
|
272
|
+
return structuredClone(result);
|
|
273
|
+
}
|
|
274
|
+
async writeReceipt(input) {
|
|
275
|
+
const first = this.records[0];
|
|
276
|
+
const last = this.records.at(-1);
|
|
277
|
+
if (!first || last?.type !== "RUN_COMPLETED")
|
|
278
|
+
throw new Error("A completed run is required before writing a receipt.");
|
|
279
|
+
const droppedEventCount = this.records.filter((record) => record.type === "EVENTS_DROPPED")
|
|
280
|
+
.reduce((total, record) => total + Number(record.payload.count ?? 0), 0);
|
|
281
|
+
const payload = {
|
|
282
|
+
schemaVersion: "agentcert.trusted_run_receipt.v0.1",
|
|
283
|
+
runId: this.runId,
|
|
284
|
+
collector: this.collector,
|
|
285
|
+
startedAt: first.occurredAt,
|
|
286
|
+
completedAt: last.occurredAt,
|
|
287
|
+
eventCount: this.records.length,
|
|
288
|
+
droppedEventCount,
|
|
289
|
+
firstEventHash: first.eventHash,
|
|
290
|
+
lastEventHash: last.eventHash,
|
|
291
|
+
mandateDigests: [...new Set(input.mandateDigests)].sort(),
|
|
292
|
+
actionIds: [...new Set(input.actionIds)].sort(),
|
|
293
|
+
journal: { valid: true, complete: true, sourceSigned: true, gaps: [], duplicateSequences: [], duplicateRecordIds: [], hashMismatches: [], signatureFailures: [], droppedEventCount, recoveredTailBytes: 0, errors: [] },
|
|
294
|
+
evidenceStrength: input.evidenceStrength,
|
|
295
|
+
sourcePublicKeyPem: this.signer.publicKeyPem,
|
|
296
|
+
};
|
|
297
|
+
const receiptSha256 = sha256(canonicalJson(payload));
|
|
298
|
+
await writeFile(this.receiptPath, `${JSON.stringify({ ...payload, receiptSha256, sourceSignature: signDigest(receiptSha256, this.signer) }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
299
|
+
}
|
|
300
|
+
async reconcileIfReady(client) {
|
|
301
|
+
if ((await this.queue.pending()).length > 0)
|
|
302
|
+
return false;
|
|
303
|
+
try {
|
|
304
|
+
await readFile(this.reconciliationPath, "utf8");
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (error.code !== "ENOENT")
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
let receipt;
|
|
312
|
+
try {
|
|
313
|
+
receipt = JSON.parse(await readFile(this.receiptPath, "utf8"));
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error.code === "ENOENT")
|
|
317
|
+
return false;
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
const reconciliation = await client.reconcile(this.runId, receipt);
|
|
321
|
+
await writeFile(this.reconciliationPath, `${JSON.stringify(reconciliation)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
class GatewayRequestError extends Error {
|
|
326
|
+
status;
|
|
327
|
+
constructor(status, messageText) {
|
|
328
|
+
super(messageText);
|
|
329
|
+
this.status = status;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function authenticate(request, expected) {
|
|
333
|
+
const actual = request.headers.authorization?.replace(/^Bearer\s+/i, "") ?? "";
|
|
334
|
+
const left = Buffer.from(actual);
|
|
335
|
+
const right = Buffer.from(expected);
|
|
336
|
+
if (left.length !== right.length || !timingSafeEqual(left, right))
|
|
337
|
+
throw new GatewayRequestError(401, "Gateway authentication failed.");
|
|
338
|
+
}
|
|
339
|
+
async function readJson(request, maxBytes) {
|
|
340
|
+
const chunks = [];
|
|
341
|
+
let size = 0;
|
|
342
|
+
for await (const chunk of request) {
|
|
343
|
+
const value = Buffer.from(chunk);
|
|
344
|
+
size += value.length;
|
|
345
|
+
if (size > maxBytes)
|
|
346
|
+
throw new GatewayRequestError(413, `Request body exceeds ${maxBytes} bytes.`);
|
|
347
|
+
chunks.push(value);
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
throw new GatewayRequestError(400, "Request body must be valid JSON.");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function listen(server, port, host) {
|
|
357
|
+
return new Promise((resolvePromise, reject) => server.once("error", reject).listen(port, host, resolvePromise));
|
|
358
|
+
}
|
|
359
|
+
function close(server) { return new Promise((resolvePromise, reject) => server.close((error) => error ? reject(error) : resolvePromise())); }
|
|
360
|
+
function json(response, status, body) { response.statusCode = status; response.setHeader("content-type", "application/json"); response.end(JSON.stringify(body)); }
|
|
361
|
+
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
362
|
+
function publicKeyFingerprint(publicKeyPem) { return sha256(createPublicKey(publicKeyPem).export({ type: "spki", format: "der" })); }
|
|
363
|
+
function signDigest(digest, signer) {
|
|
364
|
+
return { algorithm: "Ed25519", keyId: signer.keyId, signature: sign(null, Buffer.from(digest, "hex"), signer.privateKeyPem).toString("base64url") };
|
|
365
|
+
}
|
|
366
|
+
function identifier(value, field) { const parsed = required(value, field); if (parsed.length > 160 || !/^[A-Za-z0-9._:-]+$/.test(parsed))
|
|
367
|
+
throw new GatewayRequestError(400, `${field} must use URL-safe identifier characters.`); return parsed; }
|
|
368
|
+
function required(value, field) { if (typeof value !== "string" || !value.trim())
|
|
369
|
+
throw new GatewayRequestError(400, `${field} is required.`); return value; }
|
|
370
|
+
function object(value) { if (value === undefined)
|
|
371
|
+
return {}; if (!value || typeof value !== "object" || Array.isArray(value))
|
|
372
|
+
throw new GatewayRequestError(400, "Expected a JSON object."); return value; }
|
|
373
|
+
function strings(value) { if (value === undefined)
|
|
374
|
+
return []; if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
375
|
+
throw new GatewayRequestError(400, "Expected an array of strings."); return value; }
|
|
376
|
+
function positiveInteger(value, field) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
|
|
377
|
+
throw new GatewayRequestError(400, `${field} must be a positive integer.`); return value; }
|
|
378
|
+
function safeFileName(value) { return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 160); }
|
|
379
|
+
function message(error) { return error instanceof Error ? error.message : String(error); }
|
|
380
|
+
async function discoverRunIds(directory) {
|
|
381
|
+
const runIds = new Set();
|
|
382
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
383
|
+
if (!entry.isFile() || !entry.name.endsWith(".remote.jsonl"))
|
|
384
|
+
continue;
|
|
385
|
+
const firstLine = (await readFile(join(directory, entry.name), "utf8")).split(/\r?\n/, 1)[0];
|
|
386
|
+
if (!firstLine)
|
|
387
|
+
continue;
|
|
388
|
+
const record = JSON.parse(firstLine);
|
|
389
|
+
runIds.add(identifier(record.runId, "stored runId"));
|
|
390
|
+
}
|
|
391
|
+
return [...runIds];
|
|
392
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export type DurableActionPhase = "TRACKED" | "WAITING_APPROVAL" | "DENIED" | "EXPIRED" | "BLOCKED" | "GRANT_ISSUED" | "EXECUTION_STARTED" | "UNKNOWN_RESULT" | "EXECUTED" | "PROBED" | "COMPLETED";
|
|
2
|
+
export interface DurableWorkerProposal {
|
|
3
|
+
externalId: string;
|
|
4
|
+
actionType: string;
|
|
5
|
+
targetSystem: string;
|
|
6
|
+
expectedState?: Record<string, unknown>;
|
|
7
|
+
executionIntent: {
|
|
8
|
+
adapterId: string;
|
|
9
|
+
adapterVersionConstraint: string;
|
|
10
|
+
allowedOrigins: string[];
|
|
11
|
+
allowedOperation: string;
|
|
12
|
+
allowedResource: string;
|
|
13
|
+
approvedParameters: Record<string, unknown>;
|
|
14
|
+
outcomePredicate: Record<string, unknown>;
|
|
15
|
+
agentBuildId: string;
|
|
16
|
+
agentBuildDigest: string;
|
|
17
|
+
};
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface DurableWorkerAction {
|
|
21
|
+
id: string;
|
|
22
|
+
externalId: string;
|
|
23
|
+
status: "PENDING_APPROVAL" | "APPROVED" | "ALLOWED" | "REJECTED" | "DENIED" | "APPROVAL_EXPIRED" | "GRANT_EXPIRED" | "VERIFIED" | string;
|
|
24
|
+
actionType: string;
|
|
25
|
+
targetSystem: string;
|
|
26
|
+
expectedState?: Record<string, unknown>;
|
|
27
|
+
approvalExpiresAt?: string;
|
|
28
|
+
assuranceContext?: {
|
|
29
|
+
executionIntent?: {
|
|
30
|
+
adapterId: string;
|
|
31
|
+
adapterVersionConstraint: string;
|
|
32
|
+
allowedOrigins: string[];
|
|
33
|
+
allowedOperation: string;
|
|
34
|
+
allowedResource: string;
|
|
35
|
+
approvedParametersDigest: string;
|
|
36
|
+
outcomePredicateDigest: string;
|
|
37
|
+
agentBuildId: string;
|
|
38
|
+
agentBuildDigest: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface DurableWorkerGrant {
|
|
44
|
+
id: string;
|
|
45
|
+
status: string;
|
|
46
|
+
grant: {
|
|
47
|
+
payload?: Record<string, unknown>;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
};
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface DurableWorkerExecutionResult {
|
|
53
|
+
executionSessionId: string;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface DurableWorkerHostedReservation {
|
|
57
|
+
executionSessionId: string;
|
|
58
|
+
claim: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
export interface DurableWorkerObservation {
|
|
61
|
+
observedState: Record<string, unknown>;
|
|
62
|
+
observationMethod: "TARGET_API" | "TARGET_UI" | "TARGET_AUDIT_LOG" | "WEBHOOK" | "DATABASE_QUERY" | "THIRD_PARTY_CONFIRMATION";
|
|
63
|
+
observationSource: string;
|
|
64
|
+
evidenceReferences?: string[];
|
|
65
|
+
confidence?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface DurableActionCheckpoint {
|
|
68
|
+
schemaVersion: "witnora.durable_action_checkpoint.v0.1";
|
|
69
|
+
actionId: string;
|
|
70
|
+
phase: DurableActionPhase;
|
|
71
|
+
proposal: DurableWorkerProposal;
|
|
72
|
+
proposalSha256: string;
|
|
73
|
+
grant?: DurableWorkerGrant;
|
|
74
|
+
execution?: DurableWorkerExecutionResult;
|
|
75
|
+
observation?: DurableWorkerObservation;
|
|
76
|
+
receiptId?: string;
|
|
77
|
+
executionClaimId?: string;
|
|
78
|
+
hostedReservation?: DurableWorkerHostedReservation;
|
|
79
|
+
limitation?: string;
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
}
|
|
82
|
+
export interface ActionCheckpointStore {
|
|
83
|
+
load(actionId: string): Promise<DurableActionCheckpoint | undefined>;
|
|
84
|
+
save(checkpoint: DurableActionCheckpoint): Promise<void>;
|
|
85
|
+
list(): Promise<DurableActionCheckpoint[]>;
|
|
86
|
+
claimExecution(actionId: string, ownerId: string, claimedAt: string): Promise<{
|
|
87
|
+
acquired: boolean;
|
|
88
|
+
claimId: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
export declare class FileActionCheckpointStore implements ActionCheckpointStore {
|
|
92
|
+
private readonly directory;
|
|
93
|
+
constructor(directory: string);
|
|
94
|
+
load(actionId: string): Promise<DurableActionCheckpoint | undefined>;
|
|
95
|
+
save(checkpoint: DurableActionCheckpoint): Promise<void>;
|
|
96
|
+
list(): Promise<DurableActionCheckpoint[]>;
|
|
97
|
+
claimExecution(actionId: string, ownerId: string, claimedAt: string): Promise<{
|
|
98
|
+
acquired: boolean;
|
|
99
|
+
claimId: string;
|
|
100
|
+
}>;
|
|
101
|
+
private path;
|
|
102
|
+
private claimPath;
|
|
103
|
+
}
|
|
104
|
+
export interface DurableApprovedActionWorkerOptions {
|
|
105
|
+
config: {
|
|
106
|
+
adapterId: string;
|
|
107
|
+
adapterVersion: string;
|
|
108
|
+
probeId: string;
|
|
109
|
+
probeCredentialHandle: string;
|
|
110
|
+
runtimeIdentityId: string;
|
|
111
|
+
grantTtlSeconds?: number;
|
|
112
|
+
pollIntervalMs?: number;
|
|
113
|
+
};
|
|
114
|
+
store: ActionCheckpointStore;
|
|
115
|
+
hosted: {
|
|
116
|
+
getAction(actionId: string): Promise<DurableWorkerAction>;
|
|
117
|
+
issueExecutionGrant(actionId: string, input: Record<string, unknown>, idempotencyKey: string): Promise<DurableWorkerGrant>;
|
|
118
|
+
verifyAction(actionId: string, input: Record<string, unknown>, idempotencyKey: string): Promise<{
|
|
119
|
+
status: string;
|
|
120
|
+
verificationSuccess?: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
listActionReceipts(actionId: string): Promise<Array<{
|
|
123
|
+
id: string;
|
|
124
|
+
receipt?: {
|
|
125
|
+
signatureSet?: unknown[];
|
|
126
|
+
};
|
|
127
|
+
}>>;
|
|
128
|
+
claimExecutionGrant(executionGrantId: string, claim: Record<string, unknown>, idempotencyKey: string): Promise<{
|
|
129
|
+
acquired: boolean;
|
|
130
|
+
}>;
|
|
131
|
+
};
|
|
132
|
+
runtime: {
|
|
133
|
+
reconcileReadOnly: true;
|
|
134
|
+
prepareClaim(input: {
|
|
135
|
+
action: DurableWorkerAction;
|
|
136
|
+
proposal: DurableWorkerProposal;
|
|
137
|
+
grant: DurableWorkerGrant;
|
|
138
|
+
}): Promise<DurableWorkerHostedReservation>;
|
|
139
|
+
execute(input: {
|
|
140
|
+
action: DurableWorkerAction;
|
|
141
|
+
proposal: DurableWorkerProposal;
|
|
142
|
+
grant: DurableWorkerGrant;
|
|
143
|
+
hostedReservation: DurableWorkerHostedReservation;
|
|
144
|
+
}): Promise<DurableWorkerExecutionResult>;
|
|
145
|
+
reconcile(input: {
|
|
146
|
+
action: DurableWorkerAction;
|
|
147
|
+
proposal: DurableWorkerProposal;
|
|
148
|
+
grant: DurableWorkerGrant;
|
|
149
|
+
}): Promise<DurableWorkerExecutionResult | undefined>;
|
|
150
|
+
};
|
|
151
|
+
probe: {
|
|
152
|
+
id: string;
|
|
153
|
+
credentialHandle: string;
|
|
154
|
+
readOnly: true;
|
|
155
|
+
observe(input: {
|
|
156
|
+
action: DurableWorkerAction;
|
|
157
|
+
proposal: DurableWorkerProposal;
|
|
158
|
+
grant: DurableWorkerGrant;
|
|
159
|
+
execution: DurableWorkerExecutionResult;
|
|
160
|
+
}): Promise<DurableWorkerObservation>;
|
|
161
|
+
};
|
|
162
|
+
now?: () => Date;
|
|
163
|
+
}
|
|
164
|
+
export interface DurableActionWorkerStatus {
|
|
165
|
+
schemaVersion: "witnora.durable_action_worker_status.v0.1";
|
|
166
|
+
ready: boolean;
|
|
167
|
+
trackedActionCount: number;
|
|
168
|
+
pendingActionCount: number;
|
|
169
|
+
unknownResultCount: number;
|
|
170
|
+
lastHeartbeatAt: string;
|
|
171
|
+
lastError?: string;
|
|
172
|
+
}
|
|
173
|
+
export declare class DurableApprovedActionWorker {
|
|
174
|
+
private readonly options;
|
|
175
|
+
private readonly now;
|
|
176
|
+
private readonly chains;
|
|
177
|
+
private timer?;
|
|
178
|
+
private lastHeartbeatAt;
|
|
179
|
+
private lastError?;
|
|
180
|
+
private readonly workerId;
|
|
181
|
+
constructor(options: DurableApprovedActionWorkerOptions);
|
|
182
|
+
track(input: {
|
|
183
|
+
actionId: string;
|
|
184
|
+
proposal: DurableWorkerProposal;
|
|
185
|
+
}): Promise<DurableActionCheckpoint>;
|
|
186
|
+
tick(actionId: string): Promise<DurableActionCheckpoint>;
|
|
187
|
+
start(): void;
|
|
188
|
+
stop(): void;
|
|
189
|
+
status(): Promise<DurableActionWorkerStatus>;
|
|
190
|
+
private tickOnce;
|
|
191
|
+
private persist;
|
|
192
|
+
}
|
|
193
|
+
export declare function runtimeAdapterVersionSatisfies(version: string, constraint: string): boolean;
|
|
194
|
+
//# sourceMappingURL=durable-action-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-action-worker.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/durable-action-worker.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,kBAAkB,GAClB,QAAQ,GACR,SAAS,GACT,SAAS,GACT,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,UAAU,GACV,QAAQ,GACR,WAAW,CAAC;AAEhB,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,eAAe,EAAE;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,wBAAwB,EAAE,MAAM,CAAC;QACjC,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,eAAe,EAAE,MAAM,CAAC;QACxB,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC5C,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1C,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,kBAAkB,GAAG,eAAe,GAAG,UAAU,GAAG,MAAM,CAAC;IACzI,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE;YACrC,SAAS,EAAE,MAAM,CAAC;YAClB,wBAAwB,EAAE,MAAM,CAAC;YACjC,cAAc,EAAE,MAAM,EAAE,CAAC;YACzB,gBAAgB,EAAE,MAAM,CAAC;YACzB,eAAe,EAAE,MAAM,CAAC;YACxB,wBAAwB,EAAE,MAAM,CAAC;YACjC,sBAAsB,EAAE,MAAM,CAAC;YAC/B,YAAY,EAAE,MAAM,CAAC;YACrB,gBAAgB,EAAE,MAAM,CAAC;SAC1B,CAAA;KAAE,CAAC;IACJ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACrE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC3C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,8BAA8B;IAC7C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,iBAAiB,EAAE,YAAY,GAAG,WAAW,GAAG,kBAAkB,GAAG,SAAS,GAAG,gBAAgB,GAAG,0BAA0B,CAAC;IAC/H,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,wCAAwC,CAAC;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,QAAQ,EAAE,qBAAqB,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,SAAS,CAAC,EAAE,4BAA4B,CAAC;IACzC,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,8BAA8B,CAAC;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAAC;IACrE,IAAI,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAC3C,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvH;AAED,qBAAa,yBAA0B,YAAW,qBAAqB;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,SAAS,EAAE,MAAM;IAEvB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC;IAQpE,IAAI,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD,IAAI,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAQ1C,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB3H,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAIlB;AAED,MAAM,WAAW,kCAAkC;IACjD,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE;QACN,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC1D,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAC3H,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,mBAAmB,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QACnJ,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE;gBAAE,YAAY,CAAC,EAAE,OAAO,EAAE,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC,CAAC;QAC7G,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;YAAE,QAAQ,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KACvI,CAAC;IACF,OAAO,EAAE;QACP,iBAAiB,EAAE,IAAI,CAAC;QACxB,YAAY,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,mBAAmB,CAAC;YAAC,QAAQ,EAAE,qBAAqB,CAAC;YAAC,KAAK,EAAE,kBAAkB,CAAA;SAAE,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;QAC1J,OAAO,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,mBAAmB,CAAC;YAAC,QAAQ,EAAE,qBAAqB,CAAC;YAAC,KAAK,EAAE,kBAAkB,CAAC;YAAC,iBAAiB,EAAE,8BAA8B,CAAA;SAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;QACtM,SAAS,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,mBAAmB,CAAC;YAAC,QAAQ,EAAE,qBAAqB,CAAC;YAAC,KAAK,EAAE,kBAAkB,CAAA;SAAE,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAC;KAClK,CAAC;IACF,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,IAAI,CAAC;QACf,OAAO,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,mBAAmB,CAAC;YAAC,QAAQ,EAAE,qBAAqB,CAAC;YAAC,KAAK,EAAE,kBAAkB,CAAC;YAAC,SAAS,EAAE,4BAA4B,CAAA;SAAE,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;KACzL,CAAC;IACF,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,2CAA2C,CAAC;IAC3D,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,2BAA2B;IAQ1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuD;IAC9E,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;gBAEZ,OAAO,EAAE,kCAAkC;IAUlE,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,qBAAqB,CAAA;KAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAgB3G,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAOxD,KAAK,IAAI,IAAI;IAcb,IAAI,IAAI,IAAI;IAEN,MAAM,IAAI,OAAO,CAAC,yBAAyB,CAAC;YAapC,QAAQ;YA8GR,OAAO;CAMtB;AA2DD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAY3F"}
|