witnora 0.13.3 → 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/dist/gateway.js CHANGED
@@ -1,11 +1,12 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { closeSync, openSync } from "node:fs";
4
4
  import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { loadConnection } from "./credentials.js";
8
8
  import { authorizeProjectConnection } from "./device-authorization.js";
9
+ import { inspectIsolatedOutcomeProbe, observeInIsolatedOutcomeProbe } from "./probe-process.js";
9
10
  const CONFIG_SCHEMA = "witnora.customer_gateway_setup.v0.1";
10
11
  const SECRETS_SCHEMA = "witnora.customer_gateway_local_secrets.v0.1";
11
12
  const RUNTIME_SCHEMA = "witnora.managed_gateway_runtime.v0.1";
@@ -132,27 +133,58 @@ export async function doctorCustomerGateway(options) {
132
133
  catch (error) {
133
134
  checks.push({ id: "hosted_credential", status: "FAIL", message: message(error) });
134
135
  }
136
+ if (config.runtimeWorker) {
137
+ try {
138
+ const adapterModulePath = resolve(directory, config.runtimeWorker.adapterModulePath);
139
+ const probeModulePath = resolve(directory, config.runtimeWorker.probeModulePath);
140
+ if (adapterModulePath === probeModulePath)
141
+ throw new Error("Adapter and outcome probe must use separate modules.");
142
+ const [adapterDigest, probeDigest] = await Promise.all([
143
+ readFile(adapterModulePath).then((bytes) => createHash("sha256").update(bytes).digest("hex")),
144
+ readFile(probeModulePath).then((bytes) => createHash("sha256").update(bytes).digest("hex")),
145
+ ]);
146
+ if (adapterDigest !== config.runtimeWorker.adapterModuleSha256 || probeDigest !== config.runtimeWorker.probeModuleSha256)
147
+ throw new Error("Runtime adapter or probe module digest does not match gateway.json.");
148
+ const [primary, probe] = await Promise.all([
149
+ loadConnection(config.connectionName, { configHome: options.configHome }),
150
+ loadConnection(config.runtimeWorker.probeConnectionName, { configHome: options.configHome }),
151
+ ]);
152
+ if (!primary || !probe || probe.projectId !== config.projectId || probe.server !== config.server || probe.apiKey === primary.apiKey) {
153
+ throw new Error("A separate outcome-probe credential bound to this project is required.");
154
+ }
155
+ checks.push({ id: "runtime_configuration", status: "PASS", message: "The digest-pinned adapter module and separate outcome-probe credential are present." });
156
+ }
157
+ catch (error) {
158
+ checks.push({ id: "runtime_configuration", status: "FAIL", message: message(error) });
159
+ }
160
+ }
135
161
  try {
136
- const response = await (options.fetch ?? fetch)(`http://${config.host}:${config.port}/healthz`, { signal: AbortSignal.timeout(800) });
137
- if (!response.ok)
138
- throw new Error(`Gateway health returned HTTP ${response.status}.`);
162
+ const health = await gatewayHealth(`http://${config.host}:${config.port}`, options.fetch ?? fetch, Boolean(config.runtimeWorker));
163
+ if (!health)
164
+ throw new Error("Gateway health or runtime-worker readiness was not established.");
139
165
  checks.push({ id: "process", status: "PASS", message: `Gateway is listening at http://${config.host}:${config.port}.` });
140
166
  }
141
167
  catch {
142
168
  checks.push({ id: "process", status: "WARN", message: "Gateway is not running yet. Start managed mode with `witnora gateway start`." });
143
169
  }
144
170
  }
171
+ const runtimeConfigurationReady = checks.some((check) => check.id === "runtime_configuration" && check.status === "PASS")
172
+ && checks.some((check) => check.id === "process" && check.status === "PASS");
145
173
  if (config && secrets) {
146
- checks.push({ id: "enforcement", status: "WARN", message: "Write-credential mediation is not configured; current evidence ceiling is RECORDED." });
147
- checks.push({ id: "outcome_probe", status: "WARN", message: "An independent read-only outcome probe is not configured." });
174
+ checks.push(config.runtimeWorker && runtimeConfigurationReady
175
+ ? { id: "enforcement", status: "PASS", message: `Exact adapter ${config.runtimeWorker.adapterId}@${config.runtimeWorker.adapterVersion} is configured behind the durable worker.` }
176
+ : { id: "enforcement", status: "WARN", message: "Exact write-credential mediation is not proven ready; current evidence ceiling remains RECORDED." });
177
+ checks.push(config.runtimeWorker && runtimeConfigurationReady
178
+ ? { id: "outcome_probe", status: "PASS", message: `Separate read-only probe ${config.runtimeWorker.probeId} is configured.` }
179
+ : { id: "outcome_probe", status: "WARN", message: "An independent digest-pinned probe and read-only target credential are not proven ready." });
148
180
  }
149
181
  const failed = checks.some((check) => check.status === "FAIL");
150
182
  return {
151
183
  schemaVersion: "witnora.customer_gateway_doctor.v0.1",
152
- overall: failed ? "SETUP_INCOMPLETE" : "READY_TO_RECORD",
184
+ overall: failed ? "SETUP_INCOMPLETE" : runtimeConfigurationReady ? "READY_FOR_RUNTIME" : "READY_TO_RECORD",
153
185
  checks,
154
- evidenceCeiling: "recorded",
155
- nextAction: failed ? "Run `witnora gateway init --project <project-id>` again after resolving failed checks." : "Ensure the managed Gateway is healthy, then send one sandbox run through its local event API.",
186
+ evidenceCeiling: runtimeConfigurationReady ? "outcome_verified" : "recorded",
187
+ nextAction: failed ? "Resolve the failed Gateway checks before sending another action." : runtimeConfigurationReady ? "Run the Agent normally; approved configured actions continue automatically after the human decision." : "Ensure the managed Gateway is healthy, then send one sandbox run through its local event API.",
156
188
  };
157
189
  }
158
190
  export async function runCustomerGateway(options) {
@@ -161,6 +193,7 @@ export async function runCustomerGateway(options) {
161
193
  const CustomerSourceKeyRing = remoteCollector.CustomerSourceKeyRing;
162
194
  const RemoteCollectorClient = remoteCollector.RemoteCollectorClient;
163
195
  const startCustomerOwnedCollectorGateway = collectorGateway.startCustomerOwnedCollectorGateway;
196
+ const durableWorker = configRuntimeWorkerImport();
164
197
  const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
165
198
  const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
166
199
  const secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
@@ -173,6 +206,14 @@ export async function runCustomerGateway(options) {
173
206
  const keyRing = await (await exists(keyRingPath)
174
207
  ? CustomerSourceKeyRing.open(keyRingPath)
175
208
  : CustomerSourceKeyRing.create(keyRingPath, config.collectorId));
209
+ const actionWorker = config.runtimeWorker
210
+ ? await createConfiguredRuntimeActionWorker({
211
+ directory, config, connection, configHome: options.configHome,
212
+ DurableApprovedActionWorker: (await durableWorker).DurableApprovedActionWorker,
213
+ FileActionCheckpointStore: (await durableWorker).FileActionCheckpointStore,
214
+ })
215
+ : undefined;
216
+ actionWorker?.start();
176
217
  const gateway = await startCustomerOwnedCollectorGateway({
177
218
  client: new RemoteCollectorClient({ baseUrl: connection.server, projectId: connection.projectId, apiKey: connection.apiKey }),
178
219
  keyRing,
@@ -181,13 +222,143 @@ export async function runCustomerGateway(options) {
181
222
  host: process.env.WITNORA_GATEWAY_HOST?.trim() || config.host,
182
223
  port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, config.port),
183
224
  environment: "customer-owned",
225
+ ...(actionWorker && config.runtimeWorker ? { actionWorker: {
226
+ track: (input) => isConfiguredRuntimeProposal(input.proposal, config.runtimeWorker) ? actionWorker.track(input) : Promise.resolve(undefined),
227
+ status: () => actionWorker.status(), close: () => actionWorker.stop(),
228
+ } } : {}),
184
229
  });
185
230
  process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
186
- process.stdout.write("Evidence ceiling: RECORDED. No target write credential or outcome-probe credential is loaded by this reference process.\n");
231
+ process.stdout.write(config.runtimeWorker
232
+ ? "Runtime worker: READY. Approved exact configured actions execute automatically, then use the separate read-only probe and Hosted signed receipt.\n"
233
+ : "Evidence ceiling: RECORDED. No exact target adapter and separate outcome probe are configured, so runtime writes remain fail-closed.\n");
187
234
  for (const signal of ["SIGINT", "SIGTERM"]) {
188
235
  process.once(signal, () => void gateway.close().finally(() => process.exit(0)));
189
236
  }
190
237
  }
238
+ export async function createConfiguredRuntimeActionWorker(input) {
239
+ const workerConfig = input.config.runtimeWorker;
240
+ if (!workerConfig?.enabled)
241
+ throw new Error("Runtime worker is not enabled.");
242
+ const adapterModulePath = resolve(input.directory, workerConfig.adapterModulePath);
243
+ const probeModulePath = resolve(input.directory, workerConfig.probeModulePath);
244
+ if (adapterModulePath === probeModulePath)
245
+ throw new Error("Adapter and outcome probe must use separate modules.");
246
+ const [adapterBytes, probeBytes] = await Promise.all([readFile(adapterModulePath), readFile(probeModulePath)]);
247
+ const adapterModuleDigest = createHash("sha256").update(adapterBytes).digest("hex");
248
+ const probeModuleDigest = createHash("sha256").update(probeBytes).digest("hex");
249
+ if (adapterModuleDigest !== workerConfig.adapterModuleSha256 || probeModuleDigest !== workerConfig.probeModuleSha256)
250
+ throw new Error("Runtime adapter or probe module digest does not match gateway.json; refusing to load it.");
251
+ const adapterModule = await import(`${pathToFileURL(adapterModulePath).href}?sha256=${adapterModuleDigest}`);
252
+ if (typeof adapterModule.createWitnoraRuntimeAdapter !== "function")
253
+ throw new Error("Runtime adapter module must export its factory.");
254
+ const probeConnection = await loadConnection(workerConfig.probeConnectionName, { configHome: input.configHome });
255
+ if (!probeConnection || probeConnection.projectId !== input.config.projectId || probeConnection.server !== input.config.server) {
256
+ throw new Error("The separate outcome-probe credential does not match this Gateway project and server.");
257
+ }
258
+ if (probeConnection.apiKey === input.connection.apiKey)
259
+ throw new Error("The execution Gateway and outcome probe must use separate Hosted credentials.");
260
+ const adapterContext = Object.freeze({
261
+ adapterId: workerConfig.adapterId, adapterVersion: workerConfig.adapterVersion,
262
+ projectId: input.config.projectId,
263
+ hosted: runtimeHostedTransport(input.connection, input.fetch ?? fetch),
264
+ storageDirectory: resolve(input.directory, "data", "runtime-actions"),
265
+ });
266
+ const isolatedProbeConfig = {
267
+ modulePath: probeModulePath,
268
+ moduleSha256: probeModuleDigest,
269
+ probeId: workerConfig.probeId,
270
+ projectId: input.config.projectId,
271
+ credentialHandle: workerConfig.probeTargetCredentialHandle,
272
+ };
273
+ const runtime = await adapterModule.createWitnoraRuntimeAdapter(adapterContext);
274
+ await inspectIsolatedOutcomeProbe(isolatedProbeConfig);
275
+ if (runtime.id !== workerConfig.adapterId || runtime.version !== workerConfig.adapterVersion || runtime.reconcileReadOnly !== true
276
+ || typeof runtime.prepareClaim !== "function" || typeof runtime.execute !== "function" || typeof runtime.reconcile !== "function") {
277
+ throw new Error("Runtime adapter does not match the exact configured id/version or read-only reconciliation contract.");
278
+ }
279
+ const requestFetch = input.fetch ?? fetch;
280
+ const primary = projectTransport(input.connection, requestFetch);
281
+ const verifier = projectTransport(probeConnection, requestFetch);
282
+ return new input.DurableApprovedActionWorker({
283
+ config: {
284
+ adapterId: workerConfig.adapterId, adapterVersion: workerConfig.adapterVersion, probeId: workerConfig.probeId,
285
+ probeCredentialHandle: workerConfig.probeTargetCredentialHandle,
286
+ runtimeIdentityId: workerConfig.runtimeIdentityId, grantTtlSeconds: workerConfig.grantTtlSeconds, pollIntervalMs: workerConfig.pollIntervalMs,
287
+ },
288
+ store: new input.FileActionCheckpointStore(resolve(input.directory, "data", "runtime-actions", "checkpoints")),
289
+ hosted: {
290
+ getAction: (actionId) => primary(`actions/${encodeURIComponent(actionId)}`),
291
+ issueExecutionGrant: (actionId, body, idempotencyKey) => primary(`actions/${encodeURIComponent(actionId)}/execution-grant`, { method: "POST", body, idempotencyKey }),
292
+ verifyAction: (actionId, body, idempotencyKey) => verifier(`actions/${encodeURIComponent(actionId)}/verify`, { method: "POST", body, idempotencyKey }),
293
+ listActionReceipts: async (actionId) => (await primary(`actions/${encodeURIComponent(actionId)}/receipts`)).receipts ?? [],
294
+ claimExecutionGrant: (executionGrantId, claim, idempotencyKey) => claimHostedExecutionGrant(input.connection, requestFetch, executionGrantId, claim, idempotencyKey),
295
+ },
296
+ runtime: { reconcileReadOnly: true, prepareClaim: runtime.prepareClaim, execute: runtime.execute, reconcile: runtime.reconcile },
297
+ probe: {
298
+ id: workerConfig.probeId,
299
+ credentialHandle: workerConfig.probeTargetCredentialHandle,
300
+ readOnly: true,
301
+ observe: (request) => observeInIsolatedOutcomeProbe(isolatedProbeConfig, request),
302
+ },
303
+ });
304
+ }
305
+ function projectTransport(connection, requestFetch) {
306
+ return async (suffix, options = {}) => {
307
+ const response = await requestFetch(`${connection.server}/v1/projects/${encodeURIComponent(connection.projectId)}/${suffix}`, {
308
+ method: options.method ?? "GET",
309
+ headers: { authorization: `Bearer ${connection.apiKey}`, ...(options.body ? { "content-type": "application/json" } : {}), ...(options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}) },
310
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
311
+ });
312
+ const body = await response.json().catch(() => ({}));
313
+ if (!response.ok)
314
+ throw new Error(String(body.error ?? `Witnora Hosted API returned HTTP ${response.status}.`));
315
+ return body;
316
+ };
317
+ }
318
+ function runtimeHostedTransport(connection, requestFetch) {
319
+ return {
320
+ baseUrl: connection.server,
321
+ projectId: connection.projectId,
322
+ async request(suffix, options = {}) {
323
+ if (!/^(execution-grants\/[A-Za-z0-9._:-]+\/consume|execution-attempts\/[A-Za-z0-9._:-]+\/phase|execution-sessions\/[A-Za-z0-9._:-]+\/evidence)$/.test(suffix)) {
324
+ throw new Error("Runtime adapter attempted to access a Hosted path outside its execution boundary.");
325
+ }
326
+ if ((options.method ?? "GET") !== "POST")
327
+ throw new Error("Runtime Hosted execution paths require POST.");
328
+ const response = await requestFetch(`${connection.server}/v1/runtime/projects/${encodeURIComponent(connection.projectId)}/${suffix}`, {
329
+ method: "POST",
330
+ headers: { authorization: `Bearer ${connection.apiKey}`, ...(options.body ? { "content-type": "application/json" } : {}), ...(options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}) },
331
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
332
+ });
333
+ const body = await response.json().catch(() => ({}));
334
+ if (!response.ok)
335
+ throw new Error(String(body.error ?? `Witnora Hosted runtime API returned HTTP ${response.status}.`));
336
+ return body;
337
+ },
338
+ };
339
+ }
340
+ async function claimHostedExecutionGrant(connection, requestFetch, executionGrantId, claim, idempotencyKey) {
341
+ const response = await requestFetch(`${connection.server}/v1/runtime/projects/${encodeURIComponent(connection.projectId)}/execution-grants/${encodeURIComponent(executionGrantId)}/claim`, {
342
+ method: "POST",
343
+ headers: { authorization: `Bearer ${connection.apiKey}`, "content-type": "application/json", "idempotency-key": idempotencyKey },
344
+ body: JSON.stringify(claim),
345
+ });
346
+ if (response.status === 409)
347
+ return { acquired: false };
348
+ const body = await response.json().catch(() => ({}));
349
+ if (!response.ok)
350
+ throw new Error(String(body.error ?? `Witnora Hosted grant claim returned HTTP ${response.status}.`));
351
+ return { acquired: body.status === "CLAIMED" };
352
+ }
353
+ function configRuntimeWorkerImport() {
354
+ return import(new URL("./internal/control-client/durable-action-worker.js", import.meta.url).href);
355
+ }
356
+ function isConfiguredRuntimeProposal(proposal, config) {
357
+ const intent = proposal.executionIntent;
358
+ return Boolean(intent && typeof intent === "object" && !Array.isArray(intent)
359
+ && intent.adapterId === config.adapterId
360
+ && typeof intent.adapterVersionConstraint === "string");
361
+ }
191
362
  export async function startManagedCustomerGateway(options = {}) {
192
363
  const repository = resolve(options.repository ?? process.cwd());
193
364
  const directory = resolve(repository, options.dir ?? ".witnora/gateway");
@@ -272,7 +443,7 @@ export async function statusManagedCustomerGateway(options = {}) {
272
443
  const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
273
444
  const baseUrl = `http://${config.host}:${config.port}`;
274
445
  const runtime = await readRuntime(directory);
275
- const health = await gatewayHealth(baseUrl, options.fetch ?? fetch);
446
+ const health = await gatewayHealth(baseUrl, options.fetch ?? fetch, Boolean(config.runtimeWorker));
276
447
  const base = {
277
448
  schemaVersion: "witnora.managed_gateway_status.v0.1",
278
449
  baseUrl,
@@ -387,8 +558,30 @@ function parseConfig(raw) {
387
558
  throw new Error("Gateway privacyMode must remain metadata_only.");
388
559
  if (!value.host || !Number.isSafeInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65_535)
389
560
  throw new Error("Gateway host and port are invalid.");
561
+ if (value.runtimeWorker)
562
+ validateRuntimeWorkerConfig(value.runtimeWorker);
390
563
  return value;
391
564
  }
565
+ function validateRuntimeWorkerConfig(value) {
566
+ if (value.enabled !== true || !value.adapterModulePath || !/^[a-f0-9]{64}$/.test(value.adapterModuleSha256)
567
+ || !value.probeModulePath || !/^[a-f0-9]{64}$/.test(value.probeModuleSha256)
568
+ || !value.adapterId || !/^v?\d+\.\d+\.\d+$/.test(value.adapterVersion) || !value.probeId
569
+ || !value.probeConnectionName || !credentialHandle(value.probeTargetCredentialHandle) || !value.runtimeIdentityId) {
570
+ throw new Error("gateway.json runtimeWorker requires separate exact adapter/probe module digests, adapter id/version, probe credentials, and runtime identity.");
571
+ }
572
+ if (value.adapterModulePath === value.probeModulePath || value.adapterModuleSha256 === value.probeModuleSha256)
573
+ throw new Error("runtimeWorker adapter and outcome probe modules must be independently pinned.");
574
+ if (value.adapterId === value.probeId)
575
+ throw new Error("runtimeWorker adapter and outcome probe must be separate.");
576
+ if (value.grantTtlSeconds !== undefined && (!Number.isInteger(value.grantTtlSeconds) || value.grantTtlSeconds < 15 || value.grantTtlSeconds > 300))
577
+ throw new Error("runtimeWorker grantTtlSeconds must be between 15 and 300.");
578
+ if (value.pollIntervalMs !== undefined && (!Number.isInteger(value.pollIntervalMs) || value.pollIntervalMs < 250 || value.pollIntervalMs > 30_000))
579
+ throw new Error("runtimeWorker pollIntervalMs must be between 250 and 30000.");
580
+ }
581
+ function credentialHandle(value) {
582
+ return typeof value === "string" && value.length <= 256 && /^[a-z][a-z0-9+.-]*:\/\/[^\s]+$/i.test(value)
583
+ && !/(?:ac_live_|bearer\s|private.?key|password|token=)/i.test(value);
584
+ }
392
585
  function parseSecrets(raw) {
393
586
  const value = JSON.parse(raw);
394
587
  if (value.schemaVersion !== SECRETS_SCHEMA || typeof value.gatewayToken !== "string" || value.gatewayToken.length < 32) {
@@ -443,12 +636,14 @@ function gatewayPaths(directory) {
443
636
  join(directory, "README.md"),
444
637
  ];
445
638
  }
446
- async function gatewayHealth(baseUrl, requestFetch) {
639
+ async function gatewayHealth(baseUrl, requestFetch, requireRuntimeWorker = false) {
447
640
  try {
448
641
  const response = await requestFetch(`${baseUrl}/healthz`, { signal: AbortSignal.timeout(800) });
449
642
  if (!response.ok)
450
643
  return undefined;
451
644
  const value = await response.json();
645
+ if (requireRuntimeWorker && value.actionWorker?.ready !== true)
646
+ return undefined;
452
647
  return typeof value.collectorId === "string" && value.collectorId ? { collectorId: value.collectorId } : undefined;
453
648
  }
454
649
  catch {
@@ -30,6 +30,14 @@ export interface CustomerOwnedCollectorGatewayOptions {
30
30
  flushIntervalMs?: number;
31
31
  heartbeatIntervalMs?: number;
32
32
  maxBodyBytes?: number;
33
+ actionWorker?: {
34
+ track(input: {
35
+ actionId: string;
36
+ proposal: Record<string, unknown>;
37
+ }): Promise<unknown>;
38
+ status(): Promise<Record<string, unknown>>;
39
+ close?(): void | Promise<void>;
40
+ };
33
41
  }
34
42
  export interface CustomerOwnedCollectorGateway {
35
43
  baseUrl: string;
@@ -50,6 +58,7 @@ export interface CollectorGatewayStatus {
50
58
  lastAckSequence?: number;
51
59
  lastRemoteSuccessAt?: string;
52
60
  lastRemoteError?: string;
61
+ actionWorker?: Record<string, unknown>;
53
62
  }
54
63
  export declare function startCustomerOwnedCollectorGateway(options: CustomerOwnedCollectorGatewayOptions): Promise<CustomerOwnedCollectorGateway>;
55
64
  //# sourceMappingURL=collector-gateway.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"collector-gateway.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/collector-gateway.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,qBAAqB,EAGrB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC/B,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,wBAAwB;IACvC,iBAAiB,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjJ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAClH,SAAS,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChK,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7F,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3G,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9D,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACjI;AAED,MAAM,WAAW,oCAAoC;IACnD,MAAM,EAAE,wBAAwB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,kDAAkD,CAAC;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAUD,wBAAsB,kCAAkC,CAAC,OAAO,EAAE,oCAAoC,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAqL9I"}
1
+ {"version":3,"file":"collector-gateway.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/collector-gateway.ts"],"names":[],"mappings":"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"}
@@ -84,6 +84,7 @@ export async function startCustomerOwnedCollectorGateway(options) {
84
84
  lastAckSequence,
85
85
  lastRemoteSuccessAt,
86
86
  lastRemoteError,
87
+ ...(options.actionWorker ? { actionWorker: await options.actionWorker.status() } : {}),
87
88
  };
88
89
  };
89
90
  const server = createServer(async (request, response) => {
@@ -96,7 +97,11 @@ export async function startCustomerOwnedCollectorGateway(options) {
96
97
  return json(response, 200, await flush());
97
98
  if (request.method === "POST" && url.pathname === "/v1/actions") {
98
99
  const body = await readJson(request, options.maxBodyBytes ?? 1_048_576);
99
- return json(response, 202, await options.client.proposeAction(object(body.proposal), identifier(body.idempotencyKey, "idempotencyKey")));
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);
100
105
  }
101
106
  const actionRoute = url.pathname.match(/^\/v1\/actions\/([A-Za-z0-9._:-]+)$/);
102
107
  if (request.method === "GET" && actionRoute) {
@@ -187,6 +192,7 @@ export async function startCustomerOwnedCollectorGateway(options) {
187
192
  close: async () => {
188
193
  clearInterval(flushTimer);
189
194
  clearInterval(heartbeatTimer);
195
+ await options.actionWorker?.close?.();
190
196
  await flush();
191
197
  await close(server);
192
198
  },
@@ -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":"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"}
@@ -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); }
@@ -0,0 +1,135 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function inspectIsolatedOutcomeProbe(config) {
3
+ const result = await invokeProbeChild({ mode: "health", config });
4
+ if (result.ready !== true)
5
+ throw new Error("Independent outcome-probe child did not become ready.");
6
+ }
7
+ export async function observeInIsolatedOutcomeProbe(config, input) {
8
+ const result = await invokeProbeChild({ mode: "observe", config, input });
9
+ if (!result.observation || typeof result.observation !== "object" || Array.isArray(result.observation))
10
+ throw new Error("Independent outcome-probe child returned no bounded observation.");
11
+ return result.observation;
12
+ }
13
+ async function invokeProbeChild(request) {
14
+ const serializedRequest = `${JSON.stringify(request)}\n`;
15
+ if (Buffer.byteLength(serializedRequest) > 262_144)
16
+ throw new Error("Independent outcome-probe input exceeded its bounded input limit.");
17
+ const child = spawn(process.execPath, ["--input-type=module", "--eval", PROBE_CHILD_SOURCE], {
18
+ env: {}, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
19
+ });
20
+ const stdout = [];
21
+ const stderr = [];
22
+ let stdoutBytes = 0;
23
+ let stderrBytes = 0;
24
+ child.stdout.on("data", (chunk) => {
25
+ stdoutBytes += chunk.length;
26
+ if (stdoutBytes <= 65_536)
27
+ stdout.push(chunk);
28
+ else
29
+ child.kill();
30
+ });
31
+ child.stderr.on("data", (chunk) => {
32
+ stderrBytes += chunk.length;
33
+ if (stderrBytes <= 8_192)
34
+ stderr.push(chunk);
35
+ });
36
+ const timeout = setTimeout(() => child.kill(), 10_000);
37
+ child.stdin.end(serializedRequest);
38
+ const exitCode = await new Promise((resolveExit, reject) => {
39
+ child.once("error", reject);
40
+ child.once("close", resolveExit);
41
+ }).finally(() => clearTimeout(timeout));
42
+ if (stdoutBytes > 65_536)
43
+ throw new Error("Independent outcome-probe child exceeded its bounded output limit.");
44
+ const output = Buffer.concat(stdout).toString("utf8");
45
+ let value;
46
+ try {
47
+ value = JSON.parse(output);
48
+ }
49
+ catch {
50
+ throw new Error(`Independent outcome-probe child failed (${exitCode ?? "unknown"}): ${Buffer.concat(stderr).toString("utf8").slice(0, 500) || "invalid response"}`);
51
+ }
52
+ if (exitCode !== 0 || value.ok !== true)
53
+ throw new Error(`Independent outcome-probe child failed: ${String(value.error ?? "unknown error")}`);
54
+ return value;
55
+ }
56
+ const PROBE_CHILD_SOURCE = String.raw `
57
+ import { createHash } from "node:crypto";
58
+ import { readFile } from "node:fs/promises";
59
+ import { pathToFileURL } from "node:url";
60
+
61
+ const chunks = [];
62
+ let bytes = 0;
63
+ for await (const chunk of process.stdin) {
64
+ bytes += chunk.length;
65
+ if (bytes > 262144) throw new Error("probe_input_too_large");
66
+ chunks.push(chunk);
67
+ }
68
+
69
+ try {
70
+ const request = JSON.parse(Buffer.concat(chunks).toString("utf8"));
71
+ const config = request.config ?? {};
72
+ const moduleBytes = await readFile(config.modulePath);
73
+ const digest = createHash("sha256").update(moduleBytes).digest("hex");
74
+ if (digest !== config.moduleSha256) throw new Error("probe_module_digest_mismatch");
75
+ const loaded = await import(pathToFileURL(config.modulePath).href + "?sha256=" + digest);
76
+ if (typeof loaded.createWitnoraOutcomeProbe !== "function") throw new Error("probe_factory_missing");
77
+ const context = Object.freeze({
78
+ probeId: config.probeId,
79
+ projectId: config.projectId,
80
+ targetReadCredential: Object.freeze({ handle: config.credentialHandle, access: "READ_ONLY" }),
81
+ });
82
+ const probe = await loaded.createWitnoraOutcomeProbe(context);
83
+ if (probe?.id !== config.probeId || probe?.credentialHandle !== config.credentialHandle || probe?.readOnly !== true || typeof probe?.observe !== "function") throw new Error("probe_contract_mismatch");
84
+ if (request.mode === "health") {
85
+ respond({ ok: true, ready: true });
86
+ } else if (request.mode === "observe") {
87
+ const raw = await probe.observe(request.input ?? {});
88
+ const observedState = record(raw?.observedState, "observedState");
89
+ rejectSecrets(observedState);
90
+ const allowedMethods = new Set(["TARGET_API", "TARGET_UI", "TARGET_AUDIT_LOG", "WEBHOOK", "DATABASE_QUERY", "THIRD_PARTY_CONFIRMATION"]);
91
+ if (!allowedMethods.has(raw?.observationMethod) || raw?.observationSource !== config.probeId) throw new Error("probe_observation_binding_invalid");
92
+ const references = raw.evidenceReferences === undefined ? undefined : strings(raw.evidenceReferences, 20);
93
+ const confidence = raw.confidence === undefined ? undefined : Number(raw.confidence);
94
+ if (confidence !== undefined && (!Number.isFinite(confidence) || confidence < 0 || confidence > 1)) throw new Error("probe_confidence_invalid");
95
+ respond({ ok: true, observation: {
96
+ observedState,
97
+ observationMethod: raw.observationMethod,
98
+ observationSource: raw.observationSource,
99
+ ...(references ? { evidenceReferences: references } : {}),
100
+ ...(confidence === undefined ? {} : { confidence }),
101
+ } });
102
+ } else {
103
+ throw new Error("probe_mode_invalid");
104
+ }
105
+ } catch (error) {
106
+ respond({ ok: false, error: error instanceof Error ? error.message.slice(0, 200) : "probe_failed" }, 1);
107
+ }
108
+
109
+ function record(value, name) {
110
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(name + "_invalid");
111
+ return value;
112
+ }
113
+ function strings(value, maximum) {
114
+ if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || item.length > 512)) throw new Error("probe_evidence_references_invalid");
115
+ return value;
116
+ }
117
+ function rejectSecrets(value, depth = 0) {
118
+ if (depth > 8) throw new Error("probe_observation_too_deep");
119
+ for (const [key, child] of Object.entries(value)) {
120
+ if (/(authorization|cookie|password|secret|token|credential|api.?key|private.?key)/i.test(key)) throw new Error("probe_observation_contains_secret_field");
121
+ if (typeof child === "string" && child.length > 4096) throw new Error("probe_observation_string_too_large");
122
+ if (child && typeof child === "object") rejectSecrets(child, depth + 1);
123
+ }
124
+ }
125
+ function respond(value, code = 0) {
126
+ const output = JSON.stringify(value);
127
+ if (Buffer.byteLength(output) > 65536) {
128
+ process.stdout.write(JSON.stringify({ ok: false, error: "probe_output_too_large" }));
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ process.stdout.write(output);
133
+ process.exitCode = code;
134
+ }
135
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",