witnora 0.18.12 → 0.18.13

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.
@@ -53,6 +53,11 @@ export async function restartCurrentGatewayService(options) {
53
53
  await run(plan.start.command, plan.start.args);
54
54
  return plan;
55
55
  }
56
+ export async function stopCurrentGatewayService(options) {
57
+ const plan = createGatewayServicePlan(currentPlanInput(options));
58
+ await (options.run ?? runCommand)(plan.stop.command, plan.stop.args).catch(() => undefined);
59
+ return plan;
60
+ }
56
61
  export function createGatewayServicePlan(input) {
57
62
  const suffix = createHash("sha256").update(`${input.repository}\n${input.gatewayDirectory}`).digest("hex").slice(0, 12);
58
63
  const id = `witnora-gateway-${suffix}`;
package/dist/gateway.js CHANGED
@@ -609,8 +609,12 @@ export async function runCustomerGateway(options) {
609
609
  throw error;
610
610
  }
611
611
  }
612
- function configManagedWorkflowHarnessImport() {
613
- return import(new URL("./vendor/onegent-runtime/managed-workflow-harness.js", import.meta.url).href);
612
+ async function configManagedWorkflowHarnessImport() {
613
+ const [managed, runtimeWatch] = await Promise.all([
614
+ import(new URL("./vendor/onegent-runtime/managed-workflow-harness.js", import.meta.url).href),
615
+ import(new URL("./vendor/onegent-runtime/failure-runtime-watch.js", import.meta.url).href),
616
+ ]);
617
+ return { ...managed, ...runtimeWatch };
614
618
  }
615
619
  function configBusinessTaskEvaluatorImport() {
616
620
  return import(new URL("./vendor/onegent-runtime/business-task-evaluator.js", import.meta.url).href);
@@ -671,6 +675,43 @@ export async function createConfiguredWorkflowHarness(input) {
671
675
  return body.report;
672
676
  },
673
677
  });
678
+ const runtimeWatchConfiguration = input.config.schemaVersion === MANAGED_WORKFLOW_HARNESS_SCHEMA
679
+ && input.runtimeWorker?.adapterId === LOCAL_SANDBOX_ADAPTER_ID
680
+ && (!input.config.realPathActivations || (input.config.realPathActivations.length === 1
681
+ && input.config.realPathActivations[0]?.environment === "sandbox"
682
+ && input.config.realPathActivations[0].actionPathIds.length === 1));
683
+ const activation = runtimeWatchConfiguration
684
+ && input.config.schemaVersion === MANAGED_WORKFLOW_HARNESS_SCHEMA
685
+ && input.config.realPathActivations?.length === 1
686
+ && input.config.realPathActivations[0]?.environment === "sandbox"
687
+ && input.config.realPathActivations[0].actionPathIds.length === 1
688
+ ? input.config.realPathActivations[0]
689
+ : undefined;
690
+ const runtimeWatch = runtimeWatchConfiguration
691
+ && input.managed.ManagedFailureRuntimeWatch
692
+ && input.managed.FileFailureRuntimeWatchCheckpointStore
693
+ && input.managed.createFailureRuntimeWatchHostedClient
694
+ && input.managed.readLocalSandboxRuntimeWatchOutcome
695
+ ? new input.managed.ManagedFailureRuntimeWatch({
696
+ client: input.managed.createFailureRuntimeWatchHostedClient({
697
+ baseUrl: input.server,
698
+ projectId: input.projectId,
699
+ apiKey: input.apiKey,
700
+ fetch: requestFetch,
701
+ }),
702
+ checkpoints: new input.managed.FileFailureRuntimeWatchCheckpointStore(join(input.directory, "data", "failure-runtime-watch")),
703
+ intervalMs: input.config.pollIntervalMs ?? 5_000,
704
+ ...(activation ? { bindings: [{ taskContractId: activation.taskContractId, actionPathId: activation.actionPathIds[0], environment: "sandbox" }] } : {}),
705
+ readOutcome: async ({ resourceId, actions, receipts }) => input.managed.readLocalSandboxRuntimeWatchOutcome({
706
+ path: join(input.directory, "data", "runtime-sandbox", "fixture", "audit.jsonl"),
707
+ resourceId,
708
+ actions: actions.map((action) => ({
709
+ id: action.id,
710
+ executionSessionId: receipts.find((receipt) => receipt.actionId === action.id)?.receipt?.core?.executionSessionId ?? "",
711
+ })),
712
+ }),
713
+ })
714
+ : undefined;
674
715
  let timer;
675
716
  let closing = false;
676
717
  let lastTickAt;
@@ -683,12 +724,19 @@ export async function createConfiguredWorkflowHarness(input) {
683
724
  await managedEvaluator.healthCheck();
684
725
  }
685
726
  const result = await worker.tick();
727
+ const watchResult = runtimeWatch ? await runtimeWatch.tick() : undefined;
686
728
  lastTickAt = new Date().toISOString();
687
729
  lastError = (result.evaluationsFailed ?? 0) > 0 ? result.limitations?.[0] ?? "One or more evaluations failed closed." : undefined;
688
730
  if ((result.evaluationsFailed ?? 0) > 0) {
689
731
  process.stderr.write(`Managed Workflow Harness stopped ${result.evaluationsFailed} evaluation(s) fail-closed. ${lastError}\n`);
690
732
  }
691
- return result;
733
+ return watchResult ? {
734
+ ...result,
735
+ runtimeWatchCasesInspected: watchResult.casesInspected ?? 0,
736
+ runtimeWatchObservationsUploaded: watchResult.observationsUploaded ?? 0,
737
+ runtimeWatchOutcomesDeferred: watchResult.outcomesDeferred ?? 0,
738
+ runtimeWatchLimitations: watchResult.limitations ?? [],
739
+ } : result;
692
740
  }
693
741
  catch (error) {
694
742
  lastTickAt = new Date().toISOString();
@@ -751,6 +799,7 @@ export async function createContinuousAssuranceController(input) {
751
799
  server: input.server,
752
800
  apiKey: input.apiKey,
753
801
  config,
802
+ runtimeWorker: input.config.runtimeWorker,
754
803
  managed,
755
804
  evaluatorKit: input.evaluatorKit,
756
805
  fetch: request,
package/dist/onboard.js CHANGED
@@ -12,7 +12,7 @@ import { doctorCustomerGateway, activateManagedWorkflowHarness, ensureCustomerGa
12
12
  import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
13
13
  import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
14
14
  import { activateRealPathIntegrations } from "./real-path-activation.js";
15
- import { installCurrentGatewayService } from "./gateway-service.js";
15
+ import { installCurrentGatewayService, stopCurrentGatewayService } from "./gateway-service.js";
16
16
  export async function runOnboard(options) {
17
17
  const requestFetch = options.fetch ?? fetch;
18
18
  const output = options.output ?? ((message) => process.stdout.write(message));
@@ -91,6 +91,19 @@ export async function runOnboard(options) {
91
91
  start: startManagedCustomerGateway,
92
92
  stop: stopManagedCustomerGateway,
93
93
  };
94
+ const gatewayServiceLifecycle = options.gatewayServiceLifecycle ?? (!options.gatewayLifecycle ? {
95
+ stop: async ({ repository, configHome }) => {
96
+ await stopCurrentGatewayService({
97
+ repository,
98
+ configHome,
99
+ cliEntry: fileURLToPath(new URL("./cli.js", import.meta.url)),
100
+ });
101
+ },
102
+ } : undefined);
103
+ const stopGatewayOwners = async () => {
104
+ await gatewayServiceLifecycle?.stop({ repository: repositoryPath, configHome: options.configHome });
105
+ return gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
106
+ };
94
107
  let gatewayMigration;
95
108
  let managedGateway;
96
109
  let runtimeUpgrade;
@@ -184,7 +197,7 @@ export async function runOnboard(options) {
184
197
  try {
185
198
  runtimeUpgrade = await prepareRuntime(() => upgradeCustomerGatewayRuntime({ repository: repositoryPath, authorization: token, runtimeReferences: localRuntime.references, agentIdentity, configHome: options.configHome, fetch: requestFetch }));
186
199
  if (runtimeUpgrade.changed) {
187
- const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
200
+ const stopped = await stopGatewayOwners();
188
201
  if (stopped.state !== "STOPPED" || stopped.healthy)
189
202
  throw new Error("The prior managed Gateway process did not stop before Runtime generation replacement.");
190
203
  }
@@ -205,11 +218,11 @@ export async function runOnboard(options) {
205
218
  }
206
219
  }
207
220
  else {
208
- const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
221
+ const stopped = await stopGatewayOwners();
209
222
  if (stopped.state === "STOPPED" || stopped.state === "STALE") {
210
223
  output(`\nStopped the managed Gateway for project ${binding.projectId} before rebinding this repository.\n`);
211
224
  }
212
- await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
225
+ await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.collectorId, binding.projectId, token.projectId);
213
226
  gatewayMigration = await archiveGateway(gatewayState.directory, repositoryPath, binding.projectId);
214
227
  output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
215
228
  const gateway = await prepareRuntime(() => initializeCustomerGateway({
@@ -239,14 +252,14 @@ export async function runOnboard(options) {
239
252
  assuranceHarnessReadiness = { state: "WAITING_FOR_CUSTOMER_HARNESS", limitation: detail };
240
253
  }
241
254
  if (assuranceHarnessChanged && gatewayState.status !== "absent") {
242
- const stopped = await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
255
+ const stopped = await stopGatewayOwners();
243
256
  if (stopped.state !== "STOPPED" && stopped.state !== "STALE")
244
257
  throw new Error("The prior managed Gateway process did not stop before Assurance Harness activation.");
245
258
  }
246
259
  if (!options.gatewayLifecycle) {
247
260
  // Stop any detached Gateway left by an earlier CLI before the OS-owned
248
261
  // supervisor is installed. The service becomes the single process owner.
249
- await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch }).catch(() => undefined);
262
+ await stopGatewayOwners().catch(() => undefined);
250
263
  const port = await ensureCustomerGatewayPortAvailable({ repository: repositoryPath });
251
264
  if (port.changed)
252
265
  output(`Rebound this repository's generated Gateway to available localhost port ${port.port}; another project remains untouched.\n`);
@@ -304,7 +317,7 @@ export async function runOnboard(options) {
304
317
  catch (error) {
305
318
  const diagnosis = error instanceof Error ? error.message : String(error);
306
319
  if (managedGateway?.started) {
307
- await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch }).catch(() => undefined);
320
+ await stopGatewayOwners().catch(() => undefined);
308
321
  }
309
322
  await realPathActivation?.rollback().catch(() => undefined);
310
323
  await rollbackGeneratedFiles(generatedFiles);
@@ -349,18 +362,25 @@ async function inspectGatewayBinding(directory, projectId, server) {
349
362
  const existingProjectId = typeof value.projectId === "string" ? value.projectId.trim() : "";
350
363
  const existingServer = typeof value.server === "string" ? normalizeServer(value.server) : "";
351
364
  const connectionName = typeof value.connectionName === "string" ? value.connectionName.trim() : "";
365
+ const collectorId = typeof value.collectorId === "string" ? value.collectorId.trim() : "";
352
366
  const host = typeof value.host === "string" && value.host.trim() ? value.host.trim() : "127.0.0.1";
353
367
  const port = Number(value.port);
354
- if (!existingProjectId || !existingServer || !connectionName || !Number.isInteger(port) || port < 1 || port > 65_535) {
355
- throw new Error("Existing Gateway configuration is missing a valid project, server, host, or port. Preserve it and repair the setup in Advanced mode.");
368
+ if (!existingProjectId || !existingServer || !connectionName || !collectorId || !Number.isInteger(port) || port < 1 || port > 65_535) {
369
+ throw new Error("Existing Gateway configuration is missing a valid project, server, connection, collector, host, or port. Preserve it and repair the setup in Advanced mode.");
356
370
  }
357
- return { matches: existingProjectId === projectId && existingServer === server, projectId: existingProjectId, server: existingServer, connectionName, host, port };
371
+ return { matches: existingProjectId === projectId && existingServer === server, projectId: existingProjectId, server: existingServer, connectionName, collectorId, host, port };
358
372
  }
359
- async function assertGatewayStopped(requestFetch, host, port, oldProjectId, newProjectId) {
373
+ async function assertGatewayStopped(requestFetch, host, port, oldCollectorId, oldProjectId, newProjectId) {
360
374
  try {
361
375
  const response = await requestFetch(`http://${host}:${port}/healthz`, { signal: AbortSignal.timeout(800) });
362
376
  if (!response.ok)
363
377
  return;
378
+ const health = await response.json().catch(() => ({}));
379
+ // A different repository may legitimately own the old localhost port.
380
+ // It must remain untouched; this repository will select a free port after
381
+ // its prior Gateway directory is archived and regenerated.
382
+ if (health.collectorId !== oldCollectorId)
383
+ return;
364
384
  }
365
385
  catch {
366
386
  return;
@@ -0,0 +1,65 @@
1
+ export declare const FAILURE_EXPERIMENT_SUBMISSION_SCHEMA_VERSION: "witnora.failure_experiment_submission.v0.1";
2
+ export interface HostedFailureDefinition {
3
+ title: string;
4
+ taskContractId: string;
5
+ actionPathId: string;
6
+ provider: string;
7
+ environment: "sandbox" | "production";
8
+ failureSignal: string;
9
+ successSignal: string;
10
+ hardInvariants: string[];
11
+ outcomeCheck: {
12
+ kind: "READ_ONLY_PROVIDER_QUERY" | "READ_ONLY_DATABASE_QUERY" | "SIGNED_WEBHOOK_OBSERVATION" | "CUSTOMER_PROBE";
13
+ providerState: string;
14
+ expectedValue: string;
15
+ };
16
+ reproduction: {
17
+ kind: "DETERMINISTIC";
18
+ requiredRuns: number;
19
+ } | {
20
+ kind: "STATISTICAL";
21
+ runs: number;
22
+ minimumFailureRate: number;
23
+ allowedError: number;
24
+ };
25
+ }
26
+ export interface HostedFailureExperimentInput {
27
+ discoveryKey: string;
28
+ phase: "BASELINE" | "CANDIDATE" | "RUNTIME";
29
+ /** Stable source observation time; callers should reuse it on retry. */
30
+ observedAt?: string;
31
+ definition: HostedFailureDefinition;
32
+ result: {
33
+ agentVersion: string;
34
+ attempts: number;
35
+ providerCommitCount: number;
36
+ duplicateOutcomeCount: number;
37
+ failureCount: number;
38
+ hardInvariantViolations?: number;
39
+ observation: "REPORTED" | "RECORDED";
40
+ executionControl: "ENFORCED" | "NOT_ENFORCED" | "UNKNOWN";
41
+ outcome: "VERIFIED" | "NOT_VERIFIED" | "INCONCLUSIVE";
42
+ review: "NOT_REVIEWED" | "INTERNALLY_REVIEWED" | "INDEPENDENTLY_REVIEWED";
43
+ receiptIds?: string[];
44
+ };
45
+ evidence: {
46
+ summary: string;
47
+ measurements?: Record<string, string | number | boolean | null>;
48
+ limitations?: string[];
49
+ };
50
+ }
51
+ export interface HostedFailureExperimentOptions {
52
+ baseUrl: string;
53
+ projectId: string;
54
+ apiKey: string;
55
+ fetch?: typeof fetch;
56
+ }
57
+ export interface HostedFailureExperimentResult {
58
+ run: Record<string, unknown>;
59
+ evidence: Record<string, unknown>;
60
+ completion: Record<string, unknown>;
61
+ failureCase: Record<string, unknown>;
62
+ verifiedFixReport: Record<string, unknown>;
63
+ }
64
+ export declare function uploadFailureExperiment(input: HostedFailureExperimentInput, options: HostedFailureExperimentOptions): Promise<HostedFailureExperimentResult>;
65
+ //# sourceMappingURL=failure-experiment-hosted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-experiment-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/failure-experiment-hosted.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,4CAA4C,EAAG,4CAAqD,CAAC;AAElH,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,SAAS,GAAG,YAAY,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE;QACZ,IAAI,EAAE,0BAA0B,GAAG,0BAA0B,GAAG,4BAA4B,GAAG,gBAAgB,CAAC;QAChH,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,YAAY,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GACzD;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7F;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,uBAAuB,CAAC;IACpC,MAAM,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,qBAAqB,EAAE,MAAM,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;QACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,WAAW,EAAE,UAAU,GAAG,UAAU,CAAC;QACrC,gBAAgB,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,CAAC;QAC1D,OAAO,EAAE,UAAU,GAAG,cAAc,GAAG,cAAc,CAAC;QACtD,MAAM,EAAE,cAAc,GAAG,qBAAqB,GAAG,wBAAwB,CAAC;QAC1E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;QAChE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,4BAA4B,EACnC,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,6BAA6B,CAAC,CAsFxC"}
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ export const FAILURE_EXPERIMENT_SUBMISSION_SCHEMA_VERSION = "witnora.failure_experiment_submission.v0.1";
3
+ export async function uploadFailureExperiment(input, options) {
4
+ if (input.definition.environment !== "sandbox") {
5
+ throw new Error("Automatic Failure Case progression is limited to the customer sandbox.");
6
+ }
7
+ validateExperimentCounts(input.result);
8
+ const baseUrl = required(options.baseUrl, "baseUrl").replace(/\/$/, "");
9
+ const projectId = required(options.projectId, "projectId");
10
+ const apiKey = required(options.apiKey, "apiKey");
11
+ const requestFetch = options.fetch ?? fetch;
12
+ const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(projectId)}`;
13
+ const submittedAt = input.observedAt === undefined ? new Date().toISOString() : normalizedTimestamp(input.observedAt, "observedAt");
14
+ const boundedEvidence = {
15
+ schemaVersion: "witnora.failure_experiment_evidence.v0.1",
16
+ discoveryKey: input.discoveryKey,
17
+ phase: input.phase,
18
+ taskContractId: input.definition.taskContractId,
19
+ actionPathId: input.definition.actionPathId,
20
+ environment: input.definition.environment,
21
+ provider: input.definition.provider,
22
+ summary: bounded(input.evidence.summary, "evidence.summary", 1, 2_000),
23
+ measurements: boundedMeasurements(input.evidence.measurements),
24
+ limitations: (input.evidence.limitations ?? []).slice(0, 20).map((item) => bounded(item, "evidence.limitation", 1, 500)),
25
+ result: structuredClone(input.result),
26
+ observedAt: submittedAt,
27
+ rawPayloadRetainedByCustomer: true,
28
+ };
29
+ const bytes = Buffer.from(`${JSON.stringify(boundedEvidence, null, 2)}\n`, "utf8");
30
+ if (bytes.byteLength > 64 * 1024)
31
+ throw new Error("Failure experiment evidence exceeds the 64 KiB hosted boundary.");
32
+ const digest = createHash("sha256").update(bytes).digest("hex");
33
+ const operation = `failure-experiment-${digest.slice(0, 32)}`;
34
+ const run = await jsonRequest(requestFetch, `${projectUrl}/runs`, apiKey, {
35
+ method: "POST",
36
+ headers: { "idempotency-key": `${operation}:run` },
37
+ body: JSON.stringify({
38
+ externalId: `${input.discoveryKey}:${input.phase.toLowerCase()}:${digest.slice(0, 16)}`,
39
+ kind: "custom",
40
+ schemaVersion: FAILURE_EXPERIMENT_SUBMISSION_SCHEMA_VERSION,
41
+ startedAt: submittedAt,
42
+ metadata: { failureExperiment: {
43
+ schemaVersion: FAILURE_EXPERIMENT_SUBMISSION_SCHEMA_VERSION,
44
+ discoveryKey: input.discoveryKey,
45
+ phase: input.phase,
46
+ taskContractId: input.definition.taskContractId,
47
+ actionPathId: input.definition.actionPathId,
48
+ evidenceDigestSha256: digest,
49
+ rawPayloadRetainedByCustomer: true,
50
+ } },
51
+ }),
52
+ });
53
+ const runId = id(run, "Run");
54
+ const query = new URLSearchParams({
55
+ fileName: `failure-experiment-${input.phase.toLowerCase()}-evidence.json`,
56
+ kind: "failure_experiment",
57
+ schemaVersion: "witnora.failure_experiment_evidence.v0.1",
58
+ runId,
59
+ });
60
+ const evidence = await jsonRequest(requestFetch, `${projectUrl}/evidence?${query}`, apiKey, {
61
+ method: "POST",
62
+ headers: { "content-type": "application/json", "idempotency-key": `${operation}:evidence` },
63
+ body: new Uint8Array(bytes).buffer,
64
+ });
65
+ const evidenceId = id(evidence, "Evidence");
66
+ const completion = await jsonRequest(requestFetch, `${projectUrl}/runs/${encodeURIComponent(runId)}/complete`, apiKey, {
67
+ method: "POST",
68
+ headers: { "idempotency-key": `${operation}:complete` },
69
+ body: JSON.stringify({
70
+ status: input.phase === "BASELINE" ? "manual_review" : input.result.failureCount ? "failed" : "passed",
71
+ score: input.result.failureCount ? 0 : 1,
72
+ summary: boundedEvidence.summary,
73
+ metadata: { evidenceId, evidenceSha256: evidence.sha256, failureExperimentDigestSha256: digest },
74
+ }),
75
+ });
76
+ const failureCase = await jsonRequest(requestFetch, `${projectUrl}/failure-cases/ingest`, apiKey, {
77
+ method: "POST",
78
+ headers: { "idempotency-key": `${operation}:ingest` },
79
+ body: JSON.stringify({
80
+ schemaVersion: FAILURE_EXPERIMENT_SUBMISSION_SCHEMA_VERSION,
81
+ discoveryKey: input.discoveryKey,
82
+ phase: input.phase,
83
+ definition: input.definition,
84
+ result: { ...input.result, runIds: [runId], evidenceIds: [evidenceId] },
85
+ }),
86
+ });
87
+ const failureCaseId = id(failureCase, "Failure Case");
88
+ const verifiedFixReport = await jsonRequest(requestFetch, `${projectUrl}/failure-cases/${encodeURIComponent(failureCaseId)}/report`, apiKey, { method: "GET" });
89
+ return { run, evidence, completion, failureCase, verifiedFixReport };
90
+ }
91
+ function validateExperimentCounts(result) {
92
+ const counts = [
93
+ result.attempts,
94
+ result.providerCommitCount,
95
+ result.duplicateOutcomeCount,
96
+ result.failureCount,
97
+ result.hardInvariantViolations ?? 0,
98
+ ];
99
+ if (counts.some((value) => !Number.isInteger(value) || value < 0)) {
100
+ throw new Error("Failure experiment counts must be non-negative integers.");
101
+ }
102
+ if (result.attempts < 1
103
+ || result.providerCommitCount > result.attempts
104
+ || result.failureCount > result.attempts
105
+ || result.duplicateOutcomeCount > result.providerCommitCount) {
106
+ throw new Error("Failure experiment counts are internally inconsistent.");
107
+ }
108
+ }
109
+ async function jsonRequest(requestFetch, url, apiKey, init) {
110
+ const headers = new Headers(init.headers);
111
+ headers.set("authorization", `Bearer ${apiKey}`);
112
+ if (typeof init.body === "string")
113
+ headers.set("content-type", "application/json");
114
+ const response = await requestFetch(url, { ...init, headers, redirect: "error" });
115
+ const value = await response.json().catch(() => ({}));
116
+ if (!response.ok)
117
+ throw new Error(typeof value.error === "string" ? value.error : `Witnora Control Plane request failed (${response.status}).`);
118
+ return value;
119
+ }
120
+ function required(value, name) {
121
+ return bounded(value.trim(), name, 1, 2_000);
122
+ }
123
+ function normalizedTimestamp(value, name) {
124
+ if (!value.trim() || Number.isNaN(Date.parse(value)))
125
+ throw new Error(`Failure experiment ${name} must be an ISO timestamp.`);
126
+ return new Date(value).toISOString();
127
+ }
128
+ function bounded(value, name, minimum, maximum) {
129
+ if (value.length < minimum || value.length > maximum)
130
+ throw new Error(`Failure experiment ${name} must contain ${minimum}-${maximum} characters.`);
131
+ return value;
132
+ }
133
+ function boundedMeasurements(value) {
134
+ const entries = Object.entries(value ?? {}).slice(0, 50);
135
+ for (const [key, item] of entries) {
136
+ bounded(key, "evidence.measurement key", 1, 100);
137
+ if (typeof item === "string")
138
+ bounded(item, "evidence.measurement value", 0, 500);
139
+ }
140
+ return Object.fromEntries(entries);
141
+ }
142
+ function id(value, kind) {
143
+ if (typeof value.id !== "string" || !value.id)
144
+ throw new Error(`Witnora Control Plane returned ${kind} without an id.`);
145
+ return value.id;
146
+ }
@@ -0,0 +1,138 @@
1
+ import { type HostedFailureDefinition, type HostedFailureExperimentInput } from "./failure-experiment-hosted.js";
2
+ export type FailureRuntimeWatchHealth = "HEALTHY" | "REGRESSION" | "INSUFFICIENT_EVIDENCE" | "CONNECTION_UNAVAILABLE";
3
+ export interface FailureRuntimeWatchCase {
4
+ id: string;
5
+ projectId: string;
6
+ status: "FIX_VERIFIED" | "RUNTIME_PROTECTED" | "REGRESSION_DETECTED" | string;
7
+ definition: HostedFailureDefinition;
8
+ discovery?: {
9
+ discoveryKey?: string;
10
+ };
11
+ candidate?: {
12
+ agentVersion: string;
13
+ };
14
+ runtimeWatch?: {
15
+ receiptIds?: string[];
16
+ latest?: {
17
+ receiptIds?: string[];
18
+ };
19
+ };
20
+ }
21
+ export interface FailureRuntimeWatchAction {
22
+ id: string;
23
+ status: string;
24
+ expectedState?: Record<string, unknown>;
25
+ businessTaskBinding?: {
26
+ taskContractId?: string;
27
+ actionPathId?: string;
28
+ environment?: string;
29
+ agentVersion?: string;
30
+ };
31
+ }
32
+ export interface FailureRuntimeWatchReceipt {
33
+ id: string;
34
+ actionId: string;
35
+ currentStatus: string;
36
+ createdAt: string;
37
+ receipt: {
38
+ core: {
39
+ evidenceStrength?: string;
40
+ enforcementLevel?: string;
41
+ executionSessionId?: string;
42
+ };
43
+ };
44
+ }
45
+ export interface FailureRuntimeWatchOutcome {
46
+ classification: "HEALTHY" | "REGRESSION" | "INSUFFICIENT_EVIDENCE" | "CONNECTION_UNAVAILABLE" | "SCHEMA_DIGEST_DRIFT";
47
+ attempts?: number;
48
+ providerCommitCount?: number;
49
+ duplicateOutcomeCount?: number;
50
+ failureCount?: number;
51
+ hardInvariantViolations?: number;
52
+ observedAt: string;
53
+ reason?: string;
54
+ }
55
+ export type FailureRuntimeWatchOutcomeReader = (input: {
56
+ failureCase: FailureRuntimeWatchCase;
57
+ resourceId: string;
58
+ actions: FailureRuntimeWatchAction[];
59
+ receipts: FailureRuntimeWatchReceipt[];
60
+ }) => Promise<FailureRuntimeWatchOutcome>;
61
+ export interface FailureRuntimeWatchStatusInput {
62
+ health: FailureRuntimeWatchHealth;
63
+ lastCheckedAt: string;
64
+ nextCheckAt: string;
65
+ detail?: string;
66
+ }
67
+ export interface FailureRuntimeWatchHostedClient {
68
+ listFailureCases(): Promise<FailureRuntimeWatchCase[]>;
69
+ listActions(): Promise<FailureRuntimeWatchAction[]>;
70
+ listReceipts(): Promise<FailureRuntimeWatchReceipt[]>;
71
+ uploadObservation(input: HostedFailureExperimentInput & {
72
+ observedAt: string;
73
+ }): Promise<unknown>;
74
+ updateStatus(failureCaseId: string, input: FailureRuntimeWatchStatusInput): Promise<unknown>;
75
+ }
76
+ export declare function createFailureRuntimeWatchHostedClient(options: {
77
+ baseUrl: string;
78
+ projectId: string;
79
+ apiKey: string;
80
+ fetch?: typeof fetch;
81
+ }): FailureRuntimeWatchHostedClient;
82
+ export interface FailureRuntimeWatchCheckpoint {
83
+ processedReceiptIds: string[];
84
+ updatedAt: string;
85
+ }
86
+ export interface FailureRuntimeWatchCheckpointStore {
87
+ load(key: string): Promise<FailureRuntimeWatchCheckpoint | undefined>;
88
+ save(key: string, checkpoint: FailureRuntimeWatchCheckpoint): Promise<void>;
89
+ }
90
+ export declare class MemoryFailureRuntimeWatchCheckpointStore implements FailureRuntimeWatchCheckpointStore {
91
+ #private;
92
+ load(key: string): Promise<FailureRuntimeWatchCheckpoint | undefined>;
93
+ save(key: string, checkpoint: FailureRuntimeWatchCheckpoint): Promise<void>;
94
+ }
95
+ export declare class FileFailureRuntimeWatchCheckpointStore implements FailureRuntimeWatchCheckpointStore {
96
+ private readonly directory;
97
+ constructor(directory: string);
98
+ load(key: string): Promise<FailureRuntimeWatchCheckpoint | undefined>;
99
+ save(key: string, checkpoint: FailureRuntimeWatchCheckpoint): Promise<void>;
100
+ private path;
101
+ }
102
+ export interface ManagedFailureRuntimeWatchResult {
103
+ casesInspected: number;
104
+ observationsUploaded: number;
105
+ outcomesDeferred: number;
106
+ limitations: string[];
107
+ }
108
+ export declare class ManagedFailureRuntimeWatch {
109
+ #private;
110
+ constructor(options: {
111
+ client: FailureRuntimeWatchHostedClient;
112
+ checkpoints: FailureRuntimeWatchCheckpointStore;
113
+ intervalMs?: number;
114
+ now?: () => Date;
115
+ readOutcome: FailureRuntimeWatchOutcomeReader;
116
+ bindings?: Array<{
117
+ taskContractId: string;
118
+ actionPathId: string;
119
+ environment: "sandbox";
120
+ }>;
121
+ });
122
+ tick(): Promise<ManagedFailureRuntimeWatchResult>;
123
+ private runTick;
124
+ }
125
+ /**
126
+ * Reads only the local sandbox result fields required by Runtime Watch. Raw
127
+ * provider payloads and unbound audit records never leave the customer Harness.
128
+ */
129
+ export declare function readLocalSandboxRuntimeWatchOutcome(input: {
130
+ path: string;
131
+ resourceId: string;
132
+ actions: Array<{
133
+ id: string;
134
+ executionSessionId: string;
135
+ }>;
136
+ now?: () => Date;
137
+ }): Promise<FailureRuntimeWatchOutcome>;
138
+ //# sourceMappingURL=failure-runtime-watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-runtime-watch.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/failure-runtime-watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,387 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { uploadFailureExperiment } from "./failure-experiment-hosted.js";
5
+ export function createFailureRuntimeWatchHostedClient(options) {
6
+ const baseUrl = required(options.baseUrl, "baseUrl").replace(/\/$/u, "");
7
+ const projectId = required(options.projectId, "projectId");
8
+ const apiKey = required(options.apiKey, "apiKey");
9
+ const requestFetch = options.fetch ?? fetch;
10
+ const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(projectId)}`;
11
+ return {
12
+ listFailureCases: async () => list(await hostedJson(requestFetch, `${projectUrl}/failure-cases`, apiKey), "failureCases"),
13
+ listActions: async () => list(await hostedJson(requestFetch, `${projectUrl}/actions`, apiKey), "actions"),
14
+ listReceipts: async () => list(await hostedJson(requestFetch, `${projectUrl}/receipts`, apiKey), "receipts"),
15
+ uploadObservation: async (input) => uploadFailureExperiment(input, { baseUrl, projectId, apiKey, fetch: requestFetch }),
16
+ updateStatus: async (failureCaseId, input) => hostedJson(requestFetch, `${projectUrl}/failure-cases/${encodeURIComponent(required(failureCaseId, "failureCaseId"))}/runtime-watch-status`, apiKey, { method: "POST", body: JSON.stringify(input) }),
17
+ };
18
+ }
19
+ export class MemoryFailureRuntimeWatchCheckpointStore {
20
+ #checkpoints = new Map();
21
+ async load(key) {
22
+ const checkpoint = this.#checkpoints.get(key);
23
+ return checkpoint ? structuredClone(checkpoint) : undefined;
24
+ }
25
+ async save(key, checkpoint) {
26
+ this.#checkpoints.set(key, structuredClone(checkpoint));
27
+ }
28
+ }
29
+ export class FileFailureRuntimeWatchCheckpointStore {
30
+ directory;
31
+ constructor(directory) {
32
+ this.directory = directory;
33
+ if (!directory.trim())
34
+ throw new Error("Failure Runtime Watch checkpoint directory is required.");
35
+ }
36
+ async load(key) {
37
+ try {
38
+ return validateCheckpoint(JSON.parse(await readFile(this.path(key), "utf8")));
39
+ }
40
+ catch (error) {
41
+ if (error.code === "ENOENT")
42
+ return undefined;
43
+ throw error;
44
+ }
45
+ }
46
+ async save(key, checkpoint) {
47
+ const value = validateCheckpoint(checkpoint);
48
+ await mkdir(this.directory, { recursive: true });
49
+ const target = this.path(key);
50
+ const temporary = `${target}.${randomUUID()}.tmp`;
51
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
52
+ await rename(temporary, target);
53
+ }
54
+ path(key) {
55
+ if (!key.trim())
56
+ throw new Error("Failure Runtime Watch checkpoint key is required.");
57
+ return join(this.directory, `${createHash("sha256").update(key).digest("hex")}.json`);
58
+ }
59
+ }
60
+ export class ManagedFailureRuntimeWatch {
61
+ #client;
62
+ #checkpoints;
63
+ #intervalMs;
64
+ #now;
65
+ #readOutcome;
66
+ #bindings;
67
+ #activeTick;
68
+ constructor(options) {
69
+ this.#client = options.client;
70
+ this.#checkpoints = options.checkpoints;
71
+ this.#intervalMs = options.intervalMs ?? 5_000;
72
+ this.#now = options.now ?? (() => new Date());
73
+ this.#readOutcome = options.readOutcome;
74
+ this.#bindings = options.bindings ? new Set(options.bindings.map(bindingKey)) : undefined;
75
+ if (!Number.isSafeInteger(this.#intervalMs) || this.#intervalMs < 1_000 || this.#intervalMs > 60_000) {
76
+ throw new Error("Failure Runtime Watch intervalMs must be between 1000 and 60000.");
77
+ }
78
+ }
79
+ tick() {
80
+ if (this.#activeTick)
81
+ return this.#activeTick;
82
+ this.#activeTick = this.runTick().finally(() => { this.#activeTick = undefined; });
83
+ return this.#activeTick;
84
+ }
85
+ async runTick() {
86
+ const failureCases = await this.#client.listFailureCases();
87
+ const candidates = failureCases.filter((failureCase) => failureCase.definition.environment === "sandbox"
88
+ && (!this.#bindings || this.#bindings.has(bindingKey(failureCase.definition)))
89
+ && ["FIX_VERIFIED", "RUNTIME_PROTECTED", "REGRESSION_DETECTED"].includes(failureCase.status));
90
+ if (!candidates.length)
91
+ return { casesInspected: 0, observationsUploaded: 0, outcomesDeferred: 0, limitations: [] };
92
+ let actions;
93
+ let receipts;
94
+ try {
95
+ [actions, receipts] = await Promise.all([this.#client.listActions(), this.#client.listReceipts()]);
96
+ }
97
+ catch {
98
+ const detail = "Runtime Watch could not load new Action and Receipt metadata from Witnora.";
99
+ await Promise.all(candidates.map((failureCase) => {
100
+ const checkedAt = this.#now().toISOString();
101
+ return this.#client.updateStatus(failureCase.id, {
102
+ health: "CONNECTION_UNAVAILABLE",
103
+ lastCheckedAt: checkedAt,
104
+ nextCheckAt: new Date(Date.parse(checkedAt) + this.#intervalMs).toISOString(),
105
+ detail,
106
+ });
107
+ }));
108
+ return { casesInspected: candidates.length, observationsUploaded: 0, outcomesDeferred: candidates.length, limitations: [detail] };
109
+ }
110
+ let eligible = candidates;
111
+ if (!this.#bindings) {
112
+ const discoveredBindings = coveredBindingKeys(actions, receipts);
113
+ if (discoveredBindings.size !== 1) {
114
+ const detail = "Runtime Watch requires one exact sandbox Task and action path with an active enforced Receipt.";
115
+ await Promise.all(candidates.map((failureCase) => {
116
+ const checkedAt = this.#now().toISOString();
117
+ return this.#client.updateStatus(failureCase.id, {
118
+ health: "INSUFFICIENT_EVIDENCE",
119
+ lastCheckedAt: checkedAt,
120
+ nextCheckAt: new Date(Date.parse(checkedAt) + this.#intervalMs).toISOString(),
121
+ detail,
122
+ });
123
+ }));
124
+ return { casesInspected: candidates.length, observationsUploaded: 0, outcomesDeferred: candidates.length, limitations: [detail] };
125
+ }
126
+ const discoveredBinding = [...discoveredBindings][0];
127
+ eligible = candidates.filter((failureCase) => bindingKey(failureCase.definition) === discoveredBinding);
128
+ }
129
+ let observationsUploaded = 0;
130
+ let outcomesDeferred = 0;
131
+ const limitations = [];
132
+ for (const failureCase of eligible) {
133
+ const checkedAt = this.#now().toISOString();
134
+ const nextCheckAt = new Date(Date.parse(checkedAt) + this.#intervalMs).toISOString();
135
+ const checkpoint = await this.#checkpoints.load(failureCase.id) ?? {
136
+ processedReceiptIds: failureCase.runtimeWatch?.receiptIds ?? failureCase.runtimeWatch?.latest?.receiptIds ?? [],
137
+ updatedAt: checkedAt,
138
+ };
139
+ const covered = coveredOutcomes(failureCase, actions, receipts);
140
+ const pending = covered.filter(({ receipt }) => !checkpoint.processedReceiptIds.includes(receipt.id));
141
+ if (!pending.length) {
142
+ await this.#client.updateStatus(failureCase.id, {
143
+ health: failureCase.status === "REGRESSION_DETECTED" ? "REGRESSION" : "HEALTHY",
144
+ lastCheckedAt: checkedAt,
145
+ nextCheckAt,
146
+ });
147
+ continue;
148
+ }
149
+ const resourceId = pending[0].resourceId;
150
+ const group = covered.filter((item) => item.resourceId === resourceId);
151
+ const outcome = await this.#readOutcome({
152
+ failureCase,
153
+ resourceId,
154
+ actions: group.map((item) => item.action),
155
+ receipts: group.map((item) => item.receipt),
156
+ });
157
+ if (outcome.classification !== "HEALTHY" && outcome.classification !== "REGRESSION") {
158
+ outcomesDeferred += 1;
159
+ limitations.push(outcome.reason ?? `Failure Runtime Watch deferred ${failureCase.id}: ${outcome.classification}.`);
160
+ await this.#client.updateStatus(failureCase.id, {
161
+ health: outcome.classification === "SCHEMA_DIGEST_DRIFT" ? "INSUFFICIENT_EVIDENCE" : outcome.classification,
162
+ lastCheckedAt: checkedAt,
163
+ nextCheckAt,
164
+ ...(outcome.reason ? { detail: outcome.reason } : {}),
165
+ });
166
+ continue;
167
+ }
168
+ const receiptIds = group.map((item) => item.receipt.id);
169
+ const agentVersion = requiredAgentVersion(failureCase, group.map((item) => item.action));
170
+ await this.#client.uploadObservation({
171
+ discoveryKey: failureCase.discovery?.discoveryKey ?? failureCase.id,
172
+ phase: "RUNTIME",
173
+ observedAt: outcome.observedAt,
174
+ definition: structuredClone(failureCase.definition),
175
+ result: {
176
+ agentVersion,
177
+ attempts: outcome.attempts ?? group.length,
178
+ providerCommitCount: outcome.providerCommitCount ?? 0,
179
+ duplicateOutcomeCount: outcome.duplicateOutcomeCount ?? 0,
180
+ failureCount: outcome.failureCount ?? 0,
181
+ hardInvariantViolations: outcome.hardInvariantViolations ?? 0,
182
+ observation: "RECORDED",
183
+ executionControl: "ENFORCED",
184
+ outcome: "VERIFIED",
185
+ review: "NOT_REVIEWED",
186
+ receiptIds,
187
+ },
188
+ evidence: {
189
+ summary: outcome.classification === "REGRESSION"
190
+ ? "A covered sandbox result reproduced the defined failure after the candidate was verified."
191
+ : "A covered sandbox result remained healthy after the candidate was verified.",
192
+ measurements: {
193
+ providerCommitCount: outcome.providerCommitCount ?? 0,
194
+ duplicateOutcomeCount: outcome.duplicateOutcomeCount ?? 0,
195
+ failureCount: outcome.failureCount ?? 0,
196
+ hardInvariantViolations: outcome.hardInvariantViolations ?? 0,
197
+ receiptCount: receiptIds.length,
198
+ },
199
+ limitations: ["This automated observation covers the exact customer-owned sandbox task and action path only; it does not establish production reliability."],
200
+ },
201
+ });
202
+ observationsUploaded += 1;
203
+ await this.#client.updateStatus(failureCase.id, {
204
+ health: outcome.classification === "REGRESSION" ? "REGRESSION" : "HEALTHY",
205
+ lastCheckedAt: checkedAt,
206
+ nextCheckAt,
207
+ });
208
+ await this.#checkpoints.save(failureCase.id, {
209
+ processedReceiptIds: unique([...checkpoint.processedReceiptIds, ...receiptIds]),
210
+ updatedAt: checkedAt,
211
+ });
212
+ }
213
+ return { casesInspected: eligible.length, observationsUploaded, outcomesDeferred, limitations };
214
+ }
215
+ }
216
+ const MAX_LOCAL_SANDBOX_AUDIT_BYTES = 10 * 1024 * 1024;
217
+ /**
218
+ * Reads only the local sandbox result fields required by Runtime Watch. Raw
219
+ * provider payloads and unbound audit records never leave the customer Harness.
220
+ */
221
+ export async function readLocalSandboxRuntimeWatchOutcome(input) {
222
+ const observedAt = () => (input.now?.() ?? new Date()).toISOString();
223
+ const attempts = input.actions.length;
224
+ let source;
225
+ try {
226
+ const metadata = await stat(input.path);
227
+ if (!metadata.isFile() || metadata.size > MAX_LOCAL_SANDBOX_AUDIT_BYTES) {
228
+ return deferred("SCHEMA_DIGEST_DRIFT", attempts, observedAt(), "The local sandbox audit is not a bounded readable file.");
229
+ }
230
+ source = await readFile(input.path, "utf8");
231
+ }
232
+ catch (error) {
233
+ const code = error.code;
234
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
235
+ return deferred("CONNECTION_UNAVAILABLE", attempts, observedAt(), "The local sandbox audit is unavailable to Runtime Watch.");
236
+ }
237
+ throw error;
238
+ }
239
+ const expected = new Map(input.actions.map((action) => [`${action.id}\0${action.executionSessionId}`, action]));
240
+ if (expected.size !== input.actions.length || !input.resourceId || [...expected.values()].some((action) => !action.id || !action.executionSessionId)) {
241
+ return deferred("INSUFFICIENT_EVIDENCE", attempts, observedAt(), "The covered Action and execution-session bindings are incomplete.");
242
+ }
243
+ const commits = new Map();
244
+ for (const line of source.split(/\r?\n/u)) {
245
+ if (!line.trim())
246
+ continue;
247
+ let raw;
248
+ try {
249
+ raw = JSON.parse(line);
250
+ }
251
+ catch {
252
+ return deferred("SCHEMA_DIGEST_DRIFT", attempts, observedAt(), "The local sandbox audit contains invalid JSON.");
253
+ }
254
+ if (!isRecord(raw) || raw.phase !== "COMMITTED")
255
+ continue;
256
+ const record = allowlistedCommittedAuditRecord(raw);
257
+ if (!record) {
258
+ return deferred("SCHEMA_DIGEST_DRIFT", attempts, observedAt(), "A committed local sandbox result is missing an allowlisted field.");
259
+ }
260
+ const key = `${record.actionId}\0${record.executionSessionId}`;
261
+ if (record.resourceId !== input.resourceId || !expected.has(key))
262
+ continue;
263
+ const digest = createHash("sha256").update(JSON.stringify(record.observedState)).digest("hex");
264
+ if (digest !== record.stateSha256) {
265
+ return deferred("SCHEMA_DIGEST_DRIFT", attempts, observedAt(), "A covered local sandbox result failed its state digest check.");
266
+ }
267
+ const existing = commits.get(key);
268
+ if (!existing || record.at > existing.at)
269
+ commits.set(key, { at: record.at });
270
+ }
271
+ if (commits.size !== expected.size) {
272
+ return deferred("INSUFFICIENT_EVIDENCE", attempts, observedAt(), "Not every covered Action has a digest-verified committed result.");
273
+ }
274
+ const latest = [...commits.values()].map((commit) => commit.at).sort().at(-1);
275
+ const duplicateOutcomeCount = Math.max(0, commits.size - 1);
276
+ return {
277
+ classification: duplicateOutcomeCount > 0 ? "REGRESSION" : "HEALTHY",
278
+ attempts,
279
+ providerCommitCount: commits.size,
280
+ duplicateOutcomeCount,
281
+ failureCount: duplicateOutcomeCount > 0 ? 1 : 0,
282
+ hardInvariantViolations: duplicateOutcomeCount > 0 ? 1 : 0,
283
+ observedAt: latest,
284
+ };
285
+ }
286
+ function allowlistedCommittedAuditRecord(value) {
287
+ if (typeof value.resourceId !== "string"
288
+ || typeof value.actionId !== "string"
289
+ || typeof value.executionSessionId !== "string"
290
+ || !isRecord(value.observedState)
291
+ || typeof value.stateSha256 !== "string"
292
+ || !/^[a-f0-9]{64}$/u.test(value.stateSha256)
293
+ || typeof value.at !== "string"
294
+ || Number.isNaN(Date.parse(value.at)))
295
+ return undefined;
296
+ return {
297
+ resourceId: value.resourceId,
298
+ actionId: value.actionId,
299
+ executionSessionId: value.executionSessionId,
300
+ observedState: value.observedState,
301
+ stateSha256: value.stateSha256,
302
+ at: new Date(value.at).toISOString(),
303
+ };
304
+ }
305
+ function deferred(classification, attempts, observedAt, reason) {
306
+ return { classification, attempts, observedAt, reason };
307
+ }
308
+ function isRecord(value) {
309
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
310
+ }
311
+ function coveredOutcomes(failureCase, actions, receipts) {
312
+ const receiptsByAction = new Map(receipts.filter((receipt) => receipt.currentStatus === "ACTIVE"
313
+ && receipt.receipt.core.enforcementLevel === "ENFORCED"
314
+ && (receipt.receipt.core.evidenceStrength === "OUTCOME_VERIFIED" || receipt.receipt.core.evidenceStrength === "INDEPENDENTLY_REVIEWED"))
315
+ .map((receipt) => [receipt.actionId, receipt]));
316
+ return actions.flatMap((action) => {
317
+ const binding = action.businessTaskBinding;
318
+ const receipt = receiptsByAction.get(action.id);
319
+ const resourceId = typeof action.expectedState?.resourceId === "string" ? action.expectedState.resourceId : undefined;
320
+ return receipt && action.status === "VERIFIED" && resourceId
321
+ && binding?.taskContractId === failureCase.definition.taskContractId
322
+ && binding.actionPathId === failureCase.definition.actionPathId
323
+ && binding.environment === failureCase.definition.environment
324
+ ? [{ action, receipt, resourceId }]
325
+ : [];
326
+ }).sort((left, right) => left.receipt.createdAt.localeCompare(right.receipt.createdAt));
327
+ }
328
+ function coveredBindingKeys(actions, receipts) {
329
+ const coveredActionIds = new Set(receipts.filter((receipt) => receipt.currentStatus === "ACTIVE"
330
+ && receipt.receipt.core.enforcementLevel === "ENFORCED"
331
+ && (receipt.receipt.core.evidenceStrength === "OUTCOME_VERIFIED" || receipt.receipt.core.evidenceStrength === "INDEPENDENTLY_REVIEWED"))
332
+ .map((receipt) => receipt.actionId));
333
+ return new Set(actions.flatMap((action) => {
334
+ const binding = action.businessTaskBinding;
335
+ return action.status === "VERIFIED" && coveredActionIds.has(action.id)
336
+ && binding?.environment === "sandbox" && binding.taskContractId && binding.actionPathId
337
+ ? [bindingKey({ taskContractId: binding.taskContractId, actionPathId: binding.actionPathId, environment: binding.environment })]
338
+ : [];
339
+ }));
340
+ }
341
+ function requiredAgentVersion(failureCase, actions) {
342
+ const versions = new Set(actions.map((action) => action.businessTaskBinding?.agentVersion ?? failureCase.candidate?.agentVersion).filter(Boolean));
343
+ if (versions.size !== 1)
344
+ throw new Error("Failure Runtime Watch requires one exact Agent version.");
345
+ return [...versions][0];
346
+ }
347
+ function unique(values) { return [...new Set(values)]; }
348
+ function bindingKey(value) {
349
+ return `${value.taskContractId}\0${value.actionPathId}\0${value.environment}`;
350
+ }
351
+ async function hostedJson(requestFetch, url, apiKey, init = {}) {
352
+ const headers = new Headers(init.headers);
353
+ headers.set("authorization", `Bearer ${apiKey}`);
354
+ if (typeof init.body === "string")
355
+ headers.set("content-type", "application/json");
356
+ const response = await requestFetch(url, { ...init, headers, redirect: "error" });
357
+ const value = await response.json().catch(() => ({}));
358
+ if (!response.ok)
359
+ throw new Error(typeof value.error === "string" ? value.error : `Witnora Control Plane request failed (${response.status}).`);
360
+ return value;
361
+ }
362
+ function list(value, field) {
363
+ const items = value[field];
364
+ if (!Array.isArray(items) || items.length > 1_000)
365
+ throw new Error(`Witnora returned an invalid ${field} list.`);
366
+ return items;
367
+ }
368
+ function required(value, name) {
369
+ const normalized = value.trim();
370
+ if (!normalized)
371
+ throw new Error(`Failure Runtime Watch ${name} is required.`);
372
+ return normalized;
373
+ }
374
+ function validateCheckpoint(value) {
375
+ if (!value || typeof value !== "object" || Array.isArray(value))
376
+ throw new Error("Failure Runtime Watch checkpoint is invalid.");
377
+ const checkpoint = value;
378
+ if (!Array.isArray(checkpoint.processedReceiptIds)
379
+ || checkpoint.processedReceiptIds.length > 1_000
380
+ || checkpoint.processedReceiptIds.some((id) => typeof id !== "string" || !id || id.length > 200)
381
+ || new Set(checkpoint.processedReceiptIds).size !== checkpoint.processedReceiptIds.length
382
+ || typeof checkpoint.updatedAt !== "string"
383
+ || Number.isNaN(Date.parse(checkpoint.updatedAt))) {
384
+ throw new Error("Failure Runtime Watch checkpoint is invalid.");
385
+ }
386
+ return { processedReceiptIds: [...checkpoint.processedReceiptIds], updatedAt: new Date(checkpoint.updatedAt).toISOString() };
387
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.18.12",
3
+ "version": "0.18.13",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",