witnora 0.13.7 → 0.13.8

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/cli.js CHANGED
@@ -39,7 +39,7 @@ import { runOnboard } from "./onboard.js";
39
39
  import { inspectRepository } from "./onboard.js";
40
40
  import { renderReleaseEvaluation, runReleaseEvaluation } from "./release-evaluation.js";
41
41
  import { runPrivateCapabilityDiscovery } from "./private-discovery.js";
42
- import { doctorCustomerGateway, initializeCustomerGateway, isGatewayDoctorReady, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
42
+ import { configureManagedWorkflowHarness, doctorCustomerGateway, initializeCustomerGateway, isGatewayDoctorReady, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
43
43
  import { verifyEvidencePacketV02 } from "./evidence-v02.js";
44
44
  process.on("uncaughtException", reportFatalError);
45
45
  process.on("unhandledRejection", reportFatalError);
@@ -231,8 +231,25 @@ else if (command === "gateway") {
231
231
  else if (action === "run") {
232
232
  await runCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
233
233
  }
234
+ else if (action === "workflow-harness") {
235
+ const workflowIds = process.argv.flatMap((argument, index) => argument === "--workflow" && process.argv[index + 1] ? [process.argv[index + 1]] : []);
236
+ const evaluatorOrigin = readFlag("--evaluator-origin");
237
+ const evaluatorCredentialHandle = readFlag("--evaluator-credential-handle");
238
+ const evaluatorContractSha256 = readFlag("--evaluator-contract-sha256");
239
+ if (!workflowIds.length || !evaluatorOrigin || !evaluatorCredentialHandle || !evaluatorContractSha256) {
240
+ throw new Error("Use witnora gateway workflow-harness --workflow <id> --evaluator-origin http://127.0.0.1:<port>/ --evaluator-credential-handle file://... --evaluator-contract-sha256 <sha256>.");
241
+ }
242
+ const result = await configureManagedWorkflowHarness({
243
+ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir"), workflowIds,
244
+ evaluatorOrigin, evaluatorCredentialHandle, evaluatorContractSha256,
245
+ pollIntervalMs: readFlag("--poll-ms") ? Number(readFlag("--poll-ms")) : undefined,
246
+ maxConcurrency: readFlag("--max-concurrency") ? Number(readFlag("--max-concurrency")) : undefined,
247
+ force: readBoolFlag("--force"),
248
+ });
249
+ process.stdout.write(`Configured the Managed Workflow Harness for ${result.config.workflowIds.length} exact Workflow Contract(s).\nConfig: ${result.path}\nRestart the managed Gateway to activate it.\n`);
250
+ }
234
251
  else {
235
- throw new Error("Use witnora gateway init|doctor|start|status|logs|restart|stop|run.");
252
+ throw new Error("Use witnora gateway init|doctor|start|status|logs|restart|stop|run|workflow-harness.");
236
253
  }
237
254
  }
238
255
  else if (command === "discover") {
@@ -65,6 +65,8 @@ Options:
65
65
  witnora gateway restart
66
66
  witnora gateway stop
67
67
  witnora gateway run
68
+ witnora gateway workflow-harness --workflow <id> --evaluator-origin http://127.0.0.1:<port>/ \\
69
+ --evaluator-credential-handle file://... --evaluator-contract-sha256 <sha256>
68
70
 
69
71
  Initializes and manages a customer-owned, metadata-only collector beside the Agent.
70
72
  The browser approval issues a collector-scoped credential; no API key is copied into
@@ -86,6 +88,12 @@ Options:
86
88
  --force Replace an existing reviewed local setup
87
89
  --json JSON status output
88
90
  --lines <count> Number of recent log lines (default: 100)
91
+ --workflow <id> Exact active Workflow Contract (repeatable)
92
+ --evaluator-origin Literal-loopback customer evaluator origin
93
+ --evaluator-credential-handle Opaque file:// bearer credential reference
94
+ --evaluator-contract-sha256 Pinned evaluator contract digest
95
+ --poll-ms <milliseconds> Workflow plan interval (1000-60000)
96
+ --max-concurrency <count> Bounded ready evaluations (1-16)
89
97
  `;
90
98
  if (command === "design-partner")
91
99
  return `Usage:
package/dist/gateway.js CHANGED
@@ -13,6 +13,7 @@ import { generateRuntimeSandboxKit, LOCAL_SANDBOX_ADAPTER_ID, LOCAL_SANDBOX_ADAP
13
13
  const CONFIG_SCHEMA = "witnora.customer_gateway_setup.v0.1";
14
14
  const SECRETS_SCHEMA = "witnora.customer_gateway_local_secrets.v0.1";
15
15
  const RUNTIME_SCHEMA = "witnora.managed_gateway_runtime.v0.1";
16
+ const WORKFLOW_HARNESS_SCHEMA = "witnora.managed_workflow_harness.v0.1";
16
17
  export async function initializeCustomerGateway(options) {
17
18
  const repository = resolve(options.repository ?? process.cwd());
18
19
  const outDir = resolve(repository, options.outDir ?? ".witnora/gateway");
@@ -468,6 +469,10 @@ export async function runCustomerGateway(options) {
468
469
  throw new Error("The saved Gateway credential does not match gateway.json. Run gateway init again.");
469
470
  }
470
471
  const dataDirectory = resolve(directory, config.storageDirectory);
472
+ const workflowHarnessConfigPath = join(directory, "workflow-harness.json");
473
+ const workflowHarnessConfig = await exists(workflowHarnessConfigPath)
474
+ ? parseManagedWorkflowHarnessConfig(await readFile(workflowHarnessConfigPath, "utf8"))
475
+ : undefined;
471
476
  const keyRingPath = join(dataDirectory, "source-keys.json");
472
477
  const keyRing = await (await exists(keyRingPath)
473
478
  ? CustomerSourceKeyRing.open(keyRingPath)
@@ -489,6 +494,16 @@ export async function runCustomerGateway(options) {
489
494
  FileActionCheckpointStore: (await durableWorker).FileActionCheckpointStore,
490
495
  })
491
496
  : undefined;
497
+ const workflowHarness = workflowHarnessConfig
498
+ ? await createConfiguredWorkflowHarness({
499
+ directory,
500
+ projectId: config.projectId,
501
+ server: config.server,
502
+ apiKey: connection.apiKey,
503
+ config: workflowHarnessConfig,
504
+ managed: await configManagedWorkflowHarnessImport(),
505
+ })
506
+ : undefined;
492
507
  actionWorker?.start();
493
508
  const gateway = await startCustomerOwnedCollectorGateway({
494
509
  client: new RemoteCollectorClient({ baseUrl: connection.server, projectId: connection.projectId, apiKey: connection.apiKey }),
@@ -533,12 +548,15 @@ export async function runCustomerGateway(options) {
533
548
  } } : {}),
534
549
  } } : {}),
535
550
  });
551
+ workflowHarness?.start();
536
552
  process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
537
553
  process.stdout.write(config.runtimeWorker
538
554
  ? "Runtime worker: READY. Approved exact configured actions execute automatically, then use the separate read-only probe and Hosted signed receipt.\n"
539
555
  : "Evidence ceiling: RECORDED. No exact target adapter and separate outcome probe are configured, so runtime writes remain fail-closed.\n");
556
+ if (workflowHarness)
557
+ process.stdout.write("Managed Workflow Harness: CONFIGURED. Only authority-ready Replay and no-write Shadow evaluations will be sent to the pinned customer evaluator.\n");
540
558
  for (const signal of ["SIGINT", "SIGTERM"]) {
541
- process.once(signal, () => void Promise.allSettled([gateway.close(), ...(sandboxFixture ? [sandboxFixture.close()] : [])]).finally(() => process.exit(0)));
559
+ process.once(signal, () => void Promise.allSettled([gateway.close(), ...(workflowHarness ? [workflowHarness.close()] : []), ...(sandboxFixture ? [sandboxFixture.close()] : [])]).finally(() => process.exit(0)));
542
560
  }
543
561
  }
544
562
  catch (error) {
@@ -546,6 +564,75 @@ export async function runCustomerGateway(options) {
546
564
  throw error;
547
565
  }
548
566
  }
567
+ function configManagedWorkflowHarnessImport() {
568
+ return import(new URL("./vendor/onegent-runtime/managed-workflow-harness.js", import.meta.url).href);
569
+ }
570
+ export async function createConfiguredWorkflowHarness(input) {
571
+ const requestFetch = input.fetch ?? fetch;
572
+ const credential = await readSecretProviderHandle(input.config.evaluatorCredentialHandle);
573
+ const health = await requestFetch(`${input.config.evaluatorOrigin}/healthz`, {
574
+ headers: { authorization: `Bearer ${credential}` },
575
+ redirect: "error",
576
+ signal: AbortSignal.timeout(2_000),
577
+ });
578
+ const healthBody = await health.json().catch(() => ({}));
579
+ if (!health.ok
580
+ || healthBody.schemaVersion !== "witnora.business_task_evaluator.v0.1"
581
+ || healthBody.contractSha256 !== input.config.evaluatorContractSha256) {
582
+ throw new Error("Managed Workflow evaluator health did not match its pinned local contract.");
583
+ }
584
+ const hosted = input.managed.createManagedWorkflowHostedClient({
585
+ baseUrl: input.server,
586
+ projectId: input.projectId,
587
+ apiKey: input.apiKey,
588
+ workflowIds: input.config.workflowIds,
589
+ fetch: requestFetch,
590
+ });
591
+ const worker = new input.managed.ManagedBusinessWorkflowHarness({
592
+ client: hosted,
593
+ checkpoints: new input.managed.FileWorkflowEvaluationCheckpointStore(join(input.directory, "data", "workflow-evaluations")),
594
+ maxConcurrency: input.config.maxConcurrency,
595
+ evaluate: async ({ task, evaluation }) => {
596
+ const response = await requestFetch(`${input.config.evaluatorOrigin}/v1/evaluate`, {
597
+ method: "POST",
598
+ headers: { authorization: `Bearer ${credential}`, "content-type": "application/json" },
599
+ body: JSON.stringify({ schemaVersion: "witnora.business_task_evaluation_request.v0.1", task, evaluation }),
600
+ redirect: "error",
601
+ signal: AbortSignal.timeout(120_000),
602
+ });
603
+ const body = await response.json().catch(() => ({}));
604
+ if (!response.ok || !body.report || typeof body.report !== "object" || Array.isArray(body.report)) {
605
+ throw new Error(`Managed Workflow evaluator failed (${response.status}).`);
606
+ }
607
+ return body.report;
608
+ },
609
+ });
610
+ let timer;
611
+ let closing = false;
612
+ const tick = async () => {
613
+ const result = await worker.tick();
614
+ if ((result.evaluationsFailed ?? 0) > 0) {
615
+ process.stderr.write(`Managed Workflow Harness stopped ${result.evaluationsFailed} evaluation(s) fail-closed. ${result.limitations?.[0] ?? "Inspect the customer evaluator and retry."}\n`);
616
+ }
617
+ return result;
618
+ };
619
+ return {
620
+ start() {
621
+ if (timer || closing)
622
+ return;
623
+ void tick().catch((error) => process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`));
624
+ timer = setInterval(() => void tick().catch((error) => process.stderr.write(`Managed Workflow Harness tick failed: ${error instanceof Error ? error.message : "unknown error"}\n`)), input.config.pollIntervalMs ?? 5_000);
625
+ timer.unref?.();
626
+ },
627
+ async close() {
628
+ closing = true;
629
+ if (timer)
630
+ clearInterval(timer);
631
+ timer = undefined;
632
+ },
633
+ tick,
634
+ };
635
+ }
549
636
  async function runtimeSandboxFixtureReady(config) {
550
637
  try {
551
638
  if (config.adapterId !== LOCAL_SANDBOX_ADAPTER_ID || !config.sandboxOrigin || !config.probeTargetCredentialHandle || !config.fixtureContractSha256)
@@ -936,6 +1023,63 @@ function parseConfig(raw) {
936
1023
  }
937
1024
  return config;
938
1025
  }
1026
+ export function parseManagedWorkflowHarnessConfig(raw) {
1027
+ const value = JSON.parse(raw);
1028
+ if (value.schemaVersion !== WORKFLOW_HARNESS_SCHEMA
1029
+ || value.enabled !== true
1030
+ || !literalLoopbackOrigin(value.evaluatorOrigin)
1031
+ || !credentialHandle(value.evaluatorCredentialHandle ?? "")
1032
+ || !validDigest(value.evaluatorContractSha256 ?? "")
1033
+ || !Array.isArray(value.workflowIds)
1034
+ || !value.workflowIds.length
1035
+ || value.workflowIds.length > 100
1036
+ || new Set(value.workflowIds).size !== value.workflowIds.length
1037
+ || value.workflowIds.some((id) => typeof id !== "string" || !/^[A-Za-z0-9._:-]{1,200}$/.test(id))) {
1038
+ throw new Error("workflow-harness.json requires an enabled, digest-pinned literal-loopback evaluator and a local credential handle.");
1039
+ }
1040
+ if (value.pollIntervalMs !== undefined && (!Number.isInteger(value.pollIntervalMs) || value.pollIntervalMs < 1_000 || value.pollIntervalMs > 60_000)) {
1041
+ throw new Error("workflow-harness.json pollIntervalMs must be between 1000 and 60000.");
1042
+ }
1043
+ if (value.maxConcurrency !== undefined && (!Number.isInteger(value.maxConcurrency) || value.maxConcurrency < 1 || value.maxConcurrency > 16)) {
1044
+ throw new Error("workflow-harness.json maxConcurrency must be between 1 and 16.");
1045
+ }
1046
+ return value;
1047
+ }
1048
+ export async function configureManagedWorkflowHarness(options) {
1049
+ const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
1050
+ if (!await exists(join(directory, "gateway.json")))
1051
+ throw new Error("Initialize the customer-owned Gateway before configuring its Workflow Harness.");
1052
+ const config = parseManagedWorkflowHarnessConfig(JSON.stringify({
1053
+ schemaVersion: WORKFLOW_HARNESS_SCHEMA,
1054
+ enabled: true,
1055
+ evaluatorOrigin: options.evaluatorOrigin,
1056
+ evaluatorCredentialHandle: options.evaluatorCredentialHandle,
1057
+ evaluatorContractSha256: options.evaluatorContractSha256,
1058
+ workflowIds: options.workflowIds,
1059
+ ...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
1060
+ ...(options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency }),
1061
+ }));
1062
+ const path = join(directory, "workflow-harness.json");
1063
+ await writeExclusive(path, `${JSON.stringify(config, null, 2)}\n`, options.force ?? false, 0o600);
1064
+ return { path, config };
1065
+ }
1066
+ function literalLoopbackOrigin(value) {
1067
+ if (typeof value !== "string")
1068
+ return false;
1069
+ try {
1070
+ const url = new URL(value);
1071
+ return url.protocol === "http:"
1072
+ && (url.hostname === "127.0.0.1" || url.hostname === "[::1]")
1073
+ && url.username === ""
1074
+ && url.password === ""
1075
+ && url.pathname === "/"
1076
+ && url.search === ""
1077
+ && url.hash === "";
1078
+ }
1079
+ catch {
1080
+ return false;
1081
+ }
1082
+ }
939
1083
  function validateRuntimeWorkerConfig(value) {
940
1084
  if (value.enabled !== true || !value.adapterModulePath || !/^[a-f0-9]{64}$/.test(value.adapterModuleSha256)
941
1085
  || !value.probeModulePath || !/^[a-f0-9]{64}$/.test(value.probeModuleSha256)
@@ -0,0 +1,92 @@
1
+ import type { TaskEvaluationContract, TaskEvaluationReport } from "./task-evaluation.js";
2
+ export type ManagedWorkflowEvaluationMode = "REPLAY" | "SHADOW";
3
+ export interface ManagedWorkflowExecutionPlan {
4
+ schemaVersion: "witnora.business_workflow_execution_plan.v0.1";
5
+ projectId: string;
6
+ generatedAt: string;
7
+ workflow: {
8
+ id: string;
9
+ digestSha256: string;
10
+ status: "ACTIVE" | "RETIRED";
11
+ };
12
+ nodes: Array<{
13
+ id: string;
14
+ status: "BLOCKED" | "READY" | "RUNNING" | "NEEDS_DECISION" | "COMPLETED";
15
+ }>;
16
+ readyEvaluations: ManagedWorkflowReadyEvaluation[];
17
+ recordsTruncated: boolean;
18
+ safety: {
19
+ productionWrites: 0;
20
+ aggregateAssuranceClaim: false;
21
+ customerHarnessRequired: true;
22
+ };
23
+ limitations: string[];
24
+ }
25
+ export interface ManagedWorkflowReadyEvaluation {
26
+ nodeId: string;
27
+ taskContractId: string;
28
+ taskContractDigestSha256: string;
29
+ taskKey: string;
30
+ agentId: string;
31
+ agentVersion: string;
32
+ mode: ManagedWorkflowEvaluationMode;
33
+ }
34
+ export interface ManagedWorkflowHostedClient {
35
+ listActiveWorkflows(): Promise<Array<{
36
+ id: string;
37
+ digestSha256: string;
38
+ status: "ACTIVE" | "RETIRED";
39
+ }>>;
40
+ getExecutionPlan(workflowId: string): Promise<ManagedWorkflowExecutionPlan>;
41
+ getTask(taskContractId: string): Promise<TaskEvaluationContract>;
42
+ upload(report: TaskEvaluationReport, input: {
43
+ externalId: string;
44
+ }): Promise<unknown>;
45
+ }
46
+ export declare function createManagedWorkflowHostedClient(options: {
47
+ baseUrl: string;
48
+ projectId: string;
49
+ apiKey: string;
50
+ workflowIds?: string[];
51
+ fetch?: typeof fetch;
52
+ }): ManagedWorkflowHostedClient;
53
+ export interface WorkflowEvaluationCheckpointStore {
54
+ load(key: string): Promise<TaskEvaluationReport | undefined>;
55
+ save(key: string, report: TaskEvaluationReport): Promise<void>;
56
+ complete(key: string): Promise<void>;
57
+ }
58
+ export declare class MemoryWorkflowEvaluationCheckpointStore implements WorkflowEvaluationCheckpointStore {
59
+ #private;
60
+ load(key: string): Promise<TaskEvaluationReport | undefined>;
61
+ save(key: string, report: TaskEvaluationReport): Promise<void>;
62
+ complete(key: string): Promise<void>;
63
+ }
64
+ export declare class FileWorkflowEvaluationCheckpointStore implements WorkflowEvaluationCheckpointStore {
65
+ private readonly directory;
66
+ constructor(directory: string);
67
+ load(key: string): Promise<TaskEvaluationReport | undefined>;
68
+ save(key: string, report: TaskEvaluationReport): Promise<void>;
69
+ complete(key: string): Promise<void>;
70
+ private path;
71
+ }
72
+ export interface ManagedBusinessWorkflowHarnessResult {
73
+ workflowsInspected: number;
74
+ evaluationsCompleted: number;
75
+ evaluationsFailed: number;
76
+ limitations: string[];
77
+ }
78
+ export declare class ManagedBusinessWorkflowHarness {
79
+ #private;
80
+ constructor(options: {
81
+ client: ManagedWorkflowHostedClient;
82
+ checkpoints: WorkflowEvaluationCheckpointStore;
83
+ evaluate: (input: {
84
+ task: TaskEvaluationContract;
85
+ evaluation: ManagedWorkflowReadyEvaluation;
86
+ }) => Promise<TaskEvaluationReport>;
87
+ maxConcurrency?: number;
88
+ });
89
+ tick(): Promise<ManagedBusinessWorkflowHarnessResult>;
90
+ private runTick;
91
+ }
92
+ //# sourceMappingURL=managed-workflow-harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-workflow-harness.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/managed-workflow-harness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,269 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { uploadTaskEvaluationReport } from "./task-evaluation-hosted.js";
5
+ export function createManagedWorkflowHostedClient(options) {
6
+ const baseUrl = required(options.baseUrl, "baseUrl").replace(/\/$/, "");
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
+ const workflowIds = options.workflowIds?.map((id) => required(id, "workflowIds[]"));
12
+ if (workflowIds && (!workflowIds.length || workflowIds.length > 100 || new Set(workflowIds).size !== workflowIds.length)) {
13
+ throw new Error("Managed Workflow Harness workflowIds must contain 1 to 100 unique ids.");
14
+ }
15
+ return {
16
+ listActiveWorkflows: async () => {
17
+ const items = workflowIds
18
+ ? await Promise.all(workflowIds.map((id) => hostedJson(requestFetch, `${projectUrl}/business-workflows/${encodeURIComponent(id)}`, apiKey)))
19
+ : await (async () => {
20
+ const value = await hostedJson(requestFetch, `${projectUrl}/business-workflows`, apiKey);
21
+ if (!Array.isArray(value.businessWorkflows))
22
+ throw new Error("Witnora returned an invalid Business Workflow list.");
23
+ if (value.businessWorkflows.length >= 100)
24
+ throw new Error("Business Workflow listing reached its bounded limit; configure exact workflowIds before scheduling.");
25
+ return value.businessWorkflows;
26
+ })();
27
+ return items.map((item) => ({
28
+ id: required(String(item.id ?? ""), "workflow.id"),
29
+ digestSha256: digest(String(item.digestSha256 ?? ""), "workflow.digestSha256"),
30
+ status: item.status === "RETIRED" ? "RETIRED" : item.status === "ACTIVE" ? "ACTIVE" : invalidStatus(),
31
+ }));
32
+ },
33
+ getExecutionPlan: async (workflowId) => await hostedJson(requestFetch, `${projectUrl}/business-workflows/${encodeURIComponent(required(workflowId, "workflowId"))}/execution-plan`, apiKey),
34
+ getTask: async (taskContractId) => await hostedJson(requestFetch, `${projectUrl}/business-tasks/${encodeURIComponent(required(taskContractId, "taskContractId"))}`, apiKey),
35
+ upload: async (report, input) => await uploadTaskEvaluationReport(report, {
36
+ baseUrl,
37
+ projectId,
38
+ apiKey,
39
+ externalId: input.externalId,
40
+ fetch: requestFetch,
41
+ }),
42
+ };
43
+ }
44
+ export class MemoryWorkflowEvaluationCheckpointStore {
45
+ #reports = new Map();
46
+ async load(key) {
47
+ const report = this.#reports.get(key);
48
+ return report ? structuredClone(report) : undefined;
49
+ }
50
+ async save(key, report) {
51
+ this.#reports.set(key, structuredClone(report));
52
+ }
53
+ async complete(key) {
54
+ this.#reports.delete(key);
55
+ }
56
+ }
57
+ export class FileWorkflowEvaluationCheckpointStore {
58
+ directory;
59
+ constructor(directory) {
60
+ this.directory = directory;
61
+ if (!directory.trim())
62
+ throw new Error("Workflow checkpoint directory is required.");
63
+ }
64
+ async load(key) {
65
+ try {
66
+ return JSON.parse(await readFile(this.path(key), "utf8"));
67
+ }
68
+ catch (error) {
69
+ if (error.code === "ENOENT")
70
+ return undefined;
71
+ throw error;
72
+ }
73
+ }
74
+ async save(key, report) {
75
+ await mkdir(this.directory, { recursive: true });
76
+ const target = this.path(key);
77
+ const temporary = `${target}.${randomUUID()}.tmp`;
78
+ await writeFile(temporary, `${JSON.stringify(report)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
79
+ await rename(temporary, target);
80
+ }
81
+ async complete(key) {
82
+ await rm(this.path(key), { force: true });
83
+ }
84
+ path(key) {
85
+ return join(this.directory, `${createHash("sha256").update(key).digest("hex")}.json`);
86
+ }
87
+ }
88
+ export class ManagedBusinessWorkflowHarness {
89
+ #client;
90
+ #checkpoints;
91
+ #evaluate;
92
+ #maxConcurrency;
93
+ #activeTick;
94
+ constructor(options) {
95
+ this.#client = options.client;
96
+ this.#checkpoints = options.checkpoints;
97
+ this.#evaluate = options.evaluate;
98
+ this.#maxConcurrency = options.maxConcurrency ?? 4;
99
+ if (!Number.isSafeInteger(this.#maxConcurrency) || this.#maxConcurrency < 1 || this.#maxConcurrency > 16) {
100
+ throw new Error("Managed Workflow Harness maxConcurrency must be between 1 and 16.");
101
+ }
102
+ }
103
+ tick() {
104
+ if (this.#activeTick)
105
+ return this.#activeTick;
106
+ this.#activeTick = this.runTick().finally(() => { this.#activeTick = undefined; });
107
+ return this.#activeTick;
108
+ }
109
+ async runTick() {
110
+ const workflows = (await this.#client.listActiveWorkflows()).filter((workflow) => workflow.status === "ACTIVE");
111
+ let evaluationsCompleted = 0;
112
+ let evaluationsFailed = 0;
113
+ const limitations = [];
114
+ for (const workflow of workflows) {
115
+ const seen = new Set();
116
+ for (let wave = 0; wave < 100; wave += 1) {
117
+ const plan = await this.#client.getExecutionPlan(workflow.id);
118
+ validatePlan(plan, workflow);
119
+ if (plan.recordsTruncated) {
120
+ limitations.push(`Workflow ${workflow.id} was not scheduled because its authoritative Run inspection was truncated.`);
121
+ break;
122
+ }
123
+ const ready = plan.readyEvaluations.filter((evaluation) => !seen.has(checkpointKey(plan, evaluation)));
124
+ if (!ready.length)
125
+ break;
126
+ const results = await mapLimit(ready, this.#maxConcurrency, async (evaluation) => {
127
+ const key = checkpointKey(plan, evaluation);
128
+ seen.add(key);
129
+ try {
130
+ const task = await this.#client.getTask(evaluation.taskContractId);
131
+ validateTask(task, evaluation);
132
+ let report = await this.#checkpoints.load(key);
133
+ if (!report) {
134
+ report = await this.#evaluate({ task: structuredClone(task), evaluation: structuredClone(evaluation) });
135
+ validateReport(report, task, evaluation);
136
+ await this.#checkpoints.save(key, report);
137
+ }
138
+ else {
139
+ validateReport(report, task, evaluation);
140
+ }
141
+ await this.#client.upload(report, { externalId: externalId(plan, evaluation) });
142
+ await this.#checkpoints.complete(key);
143
+ return true;
144
+ }
145
+ catch {
146
+ limitations.push(`Workflow ${workflow.id} evaluation ${evaluation.nodeId}:${evaluation.mode} failed closed; no dependent evaluation was scheduled.`);
147
+ return false;
148
+ }
149
+ });
150
+ evaluationsCompleted += results.filter(Boolean).length;
151
+ evaluationsFailed += results.filter((value) => !value).length;
152
+ if (results.some((value) => !value))
153
+ break;
154
+ }
155
+ }
156
+ return { workflowsInspected: workflows.length, evaluationsCompleted, evaluationsFailed, limitations };
157
+ }
158
+ }
159
+ function validatePlan(plan, workflow) {
160
+ if (plan.schemaVersion !== "witnora.business_workflow_execution_plan.v0.1"
161
+ || plan.workflow.id !== workflow.id
162
+ || plan.workflow.digestSha256 !== workflow.digestSha256
163
+ || plan.workflow.status !== "ACTIVE"
164
+ || plan.safety.productionWrites !== 0
165
+ || plan.safety.aggregateAssuranceClaim !== false
166
+ || plan.safety.customerHarnessRequired !== true) {
167
+ throw new Error("Managed Workflow Harness rejected a mismatched or unsafe execution plan.");
168
+ }
169
+ if (plan.readyEvaluations.length > 200)
170
+ throw new Error("Managed Workflow Harness rejected an oversized execution plan.");
171
+ const nodeStatus = new Map(plan.nodes.map((node) => [node.id, node.status]));
172
+ const seen = new Set();
173
+ for (const evaluation of plan.readyEvaluations) {
174
+ const key = `${evaluation.nodeId}:${evaluation.mode}`;
175
+ if (seen.has(key)
176
+ || nodeStatus.get(evaluation.nodeId) !== "READY"
177
+ || (evaluation.mode !== "REPLAY" && evaluation.mode !== "SHADOW")
178
+ || !evaluation.taskContractId
179
+ || !evaluation.taskKey
180
+ || !evaluation.agentId
181
+ || !evaluation.agentVersion
182
+ || !/^[a-f0-9]{64}$/.test(evaluation.taskContractDigestSha256)) {
183
+ throw new Error("Managed Workflow Harness rejected an invalid ready evaluation.");
184
+ }
185
+ seen.add(key);
186
+ }
187
+ }
188
+ function validateTask(task, evaluation) {
189
+ if (task.id !== evaluation.taskContractId
190
+ || task.digestSha256 !== evaluation.taskContractDigestSha256
191
+ || task.taskKey !== evaluation.taskKey
192
+ || task.subject.agentId !== evaluation.agentId
193
+ || task.subject.agentVersion !== evaluation.agentVersion) {
194
+ throw new Error("Managed Workflow Harness rejected a Business Task that did not match the execution plan.");
195
+ }
196
+ }
197
+ function validateReport(report, task, evaluation) {
198
+ if (report.schemaVersion !== "witnora.task_evaluation.v0.1"
199
+ || report.mode !== evaluation.mode
200
+ || report.task.id !== task.id
201
+ || report.task.digestSha256 !== task.digestSha256
202
+ || report.task.subject.agentId !== task.subject.agentId
203
+ || report.task.subject.agentVersion !== task.subject.agentVersion
204
+ || report.summary.productionWrites !== 0
205
+ || (report.mode === "REPLAY" && report.classification !== "REPLAY_SANDBOX_ONLY")
206
+ || (report.mode === "SHADOW" && report.classification !== "SHADOW_NO_WRITE")) {
207
+ throw new Error("Managed Workflow Harness rejected a mismatched or unsafe evaluation report.");
208
+ }
209
+ exactKeys(report, ["schemaVersion", "mode", "classification", "generatedAt", "task", "verdict", "summary", "scenarios", "limitations"], "report");
210
+ exactKeys(report.task, ["id", "digestSha256", "taskKey", "version", "subject", "scenarioSuite"], "report.task");
211
+ exactKeys(report.task.subject, ["agentId", "agentVersion"], "report.task.subject");
212
+ exactKeys(report.task.scenarioSuite, ["id", "version", "sha256"], "report.task.scenarioSuite");
213
+ exactKeys(report.summary, ["scenarios", "matched", "changed", "blocked", "sandboxActions", "productionWrites"], "report.summary");
214
+ if (!Array.isArray(report.scenarios) || !Array.isArray(report.limitations) || report.limitations.some((value) => typeof value !== "string")) {
215
+ throw new Error("Managed Workflow Harness rejected a non-structured evaluation report.");
216
+ }
217
+ for (const scenario of report.scenarios) {
218
+ exactKeys(scenario, ["id", "source", "inputDigestSha256", "matched", "blocked", "sandboxActionCount", "criteria", "differences"], "report.scenarios[]");
219
+ if (!/^[a-f0-9]{64}$/.test(scenario.inputDigestSha256) || !Array.isArray(scenario.criteria) || !Array.isArray(scenario.differences)
220
+ || scenario.differences.some((value) => typeof value !== "string"))
221
+ throw new Error("Managed Workflow Harness rejected invalid scenario metadata.");
222
+ for (const criterion of scenario.criteria)
223
+ exactKeys(criterion, ["id", "status", "evaluator"], "report.scenarios[].criteria[]");
224
+ }
225
+ }
226
+ function exactKeys(value, allowed, name) {
227
+ const allowedKeys = new Set(allowed);
228
+ if (Object.keys(value).some((key) => !allowedKeys.has(key)))
229
+ throw new Error(`Managed Workflow Harness rejected unexpected ${name} data.`);
230
+ }
231
+ function checkpointKey(plan, evaluation) {
232
+ return `${plan.projectId}:${plan.workflow.id}:${plan.workflow.digestSha256}:${evaluation.nodeId}:${evaluation.taskContractDigestSha256}:${evaluation.mode}`;
233
+ }
234
+ function externalId(plan, evaluation) {
235
+ return `workflow:${plan.workflow.id}:${plan.workflow.digestSha256.slice(0, 16)}:${evaluation.nodeId}:${evaluation.mode.toLowerCase()}`;
236
+ }
237
+ async function mapLimit(items, limit, execute) {
238
+ const results = new Array(items.length);
239
+ let cursor = 0;
240
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
241
+ while (cursor < items.length) {
242
+ const index = cursor++;
243
+ results[index] = await execute(items[index]);
244
+ }
245
+ }));
246
+ return results;
247
+ }
248
+ async function hostedJson(requestFetch, url, apiKey) {
249
+ const response = await requestFetch(url, { headers: { authorization: `Bearer ${apiKey}` }, redirect: "error" });
250
+ const value = await response.json().catch(() => ({}));
251
+ if (!response.ok)
252
+ throw new Error(typeof value.error === "string" ? value.error : `Witnora Control Plane request failed (${response.status}).`);
253
+ return value;
254
+ }
255
+ function required(value, name) {
256
+ const normalized = value.trim();
257
+ if (!normalized)
258
+ throw new Error(`Managed Workflow Harness ${name} is required.`);
259
+ return normalized;
260
+ }
261
+ function digest(value, name) {
262
+ const normalized = value.trim().toLowerCase();
263
+ if (!/^[a-f0-9]{64}$/.test(normalized))
264
+ throw new Error(`Managed Workflow Harness ${name} must be a SHA-256 digest.`);
265
+ return normalized;
266
+ }
267
+ function invalidStatus() {
268
+ throw new Error("Witnora returned an invalid Business Workflow status.");
269
+ }
@@ -0,0 +1,83 @@
1
+ import type { TaskEvaluationReport } from "./task-evaluation.js";
2
+ export interface TaskEvaluationHostedOptions {
3
+ baseUrl: string;
4
+ projectId: string;
5
+ apiKey: string;
6
+ externalId?: string;
7
+ fetch?: typeof fetch;
8
+ }
9
+ export interface TaskEvaluationHostedResult {
10
+ run: Record<string, unknown>;
11
+ evidence: Record<string, unknown>;
12
+ completion: Record<string, unknown>;
13
+ }
14
+ export declare function uploadTaskEvaluationReport(report: TaskEvaluationReport, options: TaskEvaluationHostedOptions): Promise<TaskEvaluationHostedResult>;
15
+ export declare function createTaskEvaluationEvidenceBundle(report: TaskEvaluationReport, runId: string, reportDigestSha256?: string): {
16
+ schemaName: string;
17
+ schemaVersion: string;
18
+ schemaSemver: string;
19
+ kind: string;
20
+ runId: string;
21
+ generatedAt: string;
22
+ subject: {
23
+ name: string;
24
+ type: string;
25
+ };
26
+ verdict: {
27
+ passed: boolean;
28
+ score: number;
29
+ level: "REPLAY_SANDBOX_ONLY" | "SHADOW_NO_WRITE";
30
+ };
31
+ summary: {
32
+ products: string[];
33
+ criticalEvidence: number;
34
+ highEvidence: number;
35
+ totalEvidence: number;
36
+ };
37
+ results: {
38
+ schemaVersion: string;
39
+ product: string;
40
+ runId: string;
41
+ timestamp: string;
42
+ phase: string;
43
+ score: number;
44
+ passed: boolean;
45
+ summary: string;
46
+ artifacts: {};
47
+ evidence: {
48
+ id: string;
49
+ kind: string;
50
+ severity: string;
51
+ message: string;
52
+ source: string;
53
+ metadata: {
54
+ report: TaskEvaluationReport;
55
+ reportSha256: string;
56
+ };
57
+ }[];
58
+ }[];
59
+ evidence: {
60
+ id: string;
61
+ kind: string;
62
+ severity: string;
63
+ message: string;
64
+ source: string;
65
+ metadata: {
66
+ reportSha256: string;
67
+ taskContractId: string;
68
+ classification: "REPLAY_SANDBOX_ONLY" | "SHADOW_NO_WRITE";
69
+ };
70
+ }[];
71
+ artifacts: {};
72
+ artifactManifest: {
73
+ schemaVersion: string;
74
+ entries: never[];
75
+ };
76
+ standards: {
77
+ id: string;
78
+ name: string;
79
+ status: string;
80
+ note: string;
81
+ }[];
82
+ };
83
+ //# sourceMappingURL=task-evaluation-hosted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-evaluation-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/task-evaluation-hosted.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,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;CACrC;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAyDrC;AAED,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+B1H"}
@@ -0,0 +1,108 @@
1
+ import { createHash } from "node:crypto";
2
+ export async function uploadTaskEvaluationReport(report, options) {
3
+ const baseUrl = required(options.baseUrl, "baseUrl").replace(/\/$/, "");
4
+ const projectId = required(options.projectId, "projectId");
5
+ const apiKey = required(options.apiKey, "apiKey");
6
+ const requestFetch = options.fetch ?? fetch;
7
+ const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(projectId)}`;
8
+ const reportBytes = Buffer.from(`${JSON.stringify(report)}\n`, "utf8");
9
+ const reportDigestSha256 = createHash("sha256").update(reportBytes).digest("hex");
10
+ const operationId = `task-evaluation-${reportDigestSha256.slice(0, 32)}`;
11
+ const externalId = options.externalId?.trim() || `${report.mode.toLowerCase()}:${report.task.taskKey}:${report.task.version}:${reportDigestSha256.slice(0, 16)}`;
12
+ const run = await jsonRequest(requestFetch, `${projectUrl}/runs`, apiKey, {
13
+ method: "POST",
14
+ headers: { "idempotency-key": `${operationId}:run` },
15
+ body: JSON.stringify({
16
+ externalId,
17
+ kind: "custom",
18
+ agentId: report.task.subject.agentId,
19
+ schemaVersion: report.schemaVersion,
20
+ startedAt: report.generatedAt,
21
+ metadata: { taskEvaluation: {
22
+ taskContractId: report.task.id,
23
+ taskContractDigestSha256: report.task.digestSha256,
24
+ scenarioSuiteSha256: report.task.scenarioSuite.sha256,
25
+ reportDigestSha256,
26
+ mode: report.mode,
27
+ writesBlocked: true,
28
+ rawInputsRetainedByCustomer: true,
29
+ } },
30
+ }),
31
+ });
32
+ if (typeof run.id !== "string" || !run.id)
33
+ throw new Error("Witnora Control Plane returned a task evaluation Run without an id.");
34
+ const bundle = createTaskEvaluationEvidenceBundle(report, run.id, reportDigestSha256);
35
+ const bundleBytes = Buffer.from(`${JSON.stringify(bundle, null, 2)}\n`, "utf8");
36
+ const query = new URLSearchParams({
37
+ fileName: `business-task-${report.mode.toLowerCase()}-evidence.json`,
38
+ kind: "evidence_bundle",
39
+ schemaVersion: "agentcert.evidence.v0.1",
40
+ runId: run.id,
41
+ });
42
+ const evidence = await jsonRequest(requestFetch, `${projectUrl}/evidence?${query}`, apiKey, {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json", "idempotency-key": `${operationId}:evidence` },
45
+ body: new Uint8Array(bundleBytes).buffer,
46
+ });
47
+ const passed = report.verdict === "PASSED" || report.verdict === "MATCHED";
48
+ const completion = await jsonRequest(requestFetch, `${projectUrl}/runs/${encodeURIComponent(run.id)}/complete`, apiKey, {
49
+ method: "POST",
50
+ headers: { "idempotency-key": `${operationId}:complete` },
51
+ body: JSON.stringify({
52
+ status: passed ? "passed" : "manual_review",
53
+ score: report.summary.scenarios ? report.summary.matched / report.summary.scenarios : 0,
54
+ summary: `${report.mode} evaluated ${report.summary.scenarios} declared scenario${report.summary.scenarios === 1 ? "" : "s"}; ${report.summary.changed} changed and ${report.summary.blocked} blocked.`,
55
+ metadata: { evidenceId: evidence.id, evidenceSha256: evidence.sha256, taskEvaluationReportSha256: reportDigestSha256 },
56
+ }),
57
+ });
58
+ return { run, evidence, completion };
59
+ }
60
+ export function createTaskEvaluationEvidenceBundle(report, runId, reportDigestSha256) {
61
+ const digest = reportDigestSha256 ?? createHash("sha256").update(`${JSON.stringify(report)}\n`).digest("hex");
62
+ const passed = report.verdict === "PASSED" || report.verdict === "MATCHED";
63
+ return {
64
+ schemaName: "agentcert.evidence_bundle",
65
+ schemaVersion: "agentcert.evidence.v0.1",
66
+ schemaSemver: "0.1.0",
67
+ kind: "agentcert.evidence_bundle",
68
+ runId,
69
+ generatedAt: report.generatedAt,
70
+ subject: { name: `${report.task.taskKey} v${report.task.version}`, type: "application" },
71
+ verdict: { passed, score: passed ? 100 : 0, level: report.classification },
72
+ summary: { products: ["onegent-runtime"], criticalEvidence: report.summary.blocked, highEvidence: report.summary.changed, totalEvidence: report.summary.scenarios },
73
+ results: [{
74
+ schemaVersion: "1", product: "onegent-runtime", runId, timestamp: report.generatedAt, phase: "pre-release",
75
+ score: passed ? 100 : 0, passed,
76
+ summary: `${report.mode} comparison for the declared Business Task Contract.`, artifacts: {},
77
+ evidence: [{
78
+ id: `task-evaluation:${digest.slice(0, 16)}`, kind: "task_evaluation", severity: passed ? "info" : "high",
79
+ message: `${report.mode} ${report.verdict.toLowerCase()} for ${report.summary.scenarios} declared scenario${report.summary.scenarios === 1 ? "" : "s"}.`,
80
+ source: "onegent-runtime", metadata: { report: structuredClone(report), reportSha256: digest },
81
+ }],
82
+ }],
83
+ evidence: [{
84
+ id: `task-evaluation:${digest.slice(0, 16)}`, kind: "task_evaluation", severity: passed ? "info" : "high",
85
+ message: `${report.mode} ${report.verdict.toLowerCase()} for the declared Business Task Contract.`, source: "onegent-runtime",
86
+ metadata: { reportSha256: digest, taskContractId: report.task.id, classification: report.classification },
87
+ }],
88
+ artifacts: {}, artifactManifest: { schemaVersion: "agentcert.artifact_manifest.v0.1", entries: [] },
89
+ standards: [{ id: "witnora-business-task", name: "Witnora Business Task Contract", status: "mapped", note: report.limitations.join(" ") }],
90
+ };
91
+ }
92
+ async function jsonRequest(requestFetch, url, apiKey, init) {
93
+ const headers = new Headers(init.headers);
94
+ headers.set("authorization", `Bearer ${apiKey}`);
95
+ if (typeof init.body === "string")
96
+ headers.set("content-type", "application/json");
97
+ const response = await requestFetch(url, { ...init, headers, redirect: "error" });
98
+ const value = await response.json().catch(() => ({}));
99
+ if (!response.ok)
100
+ throw new Error(typeof value.error === "string" ? value.error : `Witnora Control Plane request failed (${response.status}).`);
101
+ return value;
102
+ }
103
+ function required(value, name) {
104
+ const normalized = value.trim();
105
+ if (!normalized)
106
+ throw new Error(`Task evaluation hosted upload ${name} is required.`);
107
+ return normalized;
108
+ }
@@ -0,0 +1,123 @@
1
+ import type { SandboxCertificationHarness } from "./sandbox-harness.js";
2
+ import type { CreateActionIntentInput } from "./types.js";
3
+ export declare const TASK_EVALUATION_SCHEMA_VERSION: "witnora.task_evaluation.v0.1";
4
+ export interface TaskEvaluationContract {
5
+ id: string;
6
+ digestSha256: string;
7
+ taskKey: string;
8
+ version: number;
9
+ subject: {
10
+ agentId: string;
11
+ agentVersion: string;
12
+ };
13
+ scenarioSuite: {
14
+ id: string;
15
+ version: string;
16
+ sha256: string;
17
+ };
18
+ actionPaths: Array<{
19
+ id: string;
20
+ actionType: "SUBMIT" | "PAY" | "SEND" | "UPDATE";
21
+ targetSystem: string;
22
+ requiredPermissions: string[];
23
+ customerDecision?: "ALLOW" | "REQUIRE_APPROVAL" | "DENY";
24
+ }>;
25
+ successCriteria: Array<{
26
+ id: string;
27
+ evaluator: "DETERMINISTIC" | "READ_ONLY_PROBE";
28
+ predicateDigestSha256: string;
29
+ }>;
30
+ }
31
+ export interface TaskEvaluationIntent {
32
+ pathId: string;
33
+ parametersDigestSha256: string;
34
+ action?: CreateActionIntentInput;
35
+ }
36
+ export interface TaskEvaluationScenario {
37
+ id: string;
38
+ source: "HISTORICAL" | "FIXTURE" | "LIVE_SHADOW";
39
+ sanitized: boolean;
40
+ input: unknown;
41
+ inputDigestSha256: string;
42
+ baseline: {
43
+ resultDigestSha256: string;
44
+ actionIntents: Array<Omit<TaskEvaluationIntent, "action">>;
45
+ };
46
+ }
47
+ export interface TaskCandidateEvaluation {
48
+ resultDigestSha256: string;
49
+ actionIntents: TaskEvaluationIntent[];
50
+ criteria: Array<{
51
+ id: string;
52
+ passed: boolean;
53
+ }>;
54
+ }
55
+ export interface ShadowCandidateEvaluation {
56
+ resultDigestSha256: string;
57
+ criteria: Array<{
58
+ id: string;
59
+ passed: boolean;
60
+ }>;
61
+ }
62
+ export interface TaskEvaluationReport {
63
+ schemaVersion: typeof TASK_EVALUATION_SCHEMA_VERSION;
64
+ mode: "REPLAY" | "SHADOW";
65
+ classification: "REPLAY_SANDBOX_ONLY" | "SHADOW_NO_WRITE";
66
+ generatedAt: string;
67
+ task: {
68
+ id: string;
69
+ digestSha256: string;
70
+ taskKey: string;
71
+ version: number;
72
+ subject: TaskEvaluationContract["subject"];
73
+ scenarioSuite: TaskEvaluationContract["scenarioSuite"];
74
+ };
75
+ verdict: "PASSED" | "MATCHED" | "CHANGED" | "BLOCKED";
76
+ summary: {
77
+ scenarios: number;
78
+ matched: number;
79
+ changed: number;
80
+ blocked: number;
81
+ sandboxActions: number;
82
+ productionWrites: 0;
83
+ };
84
+ scenarios: Array<{
85
+ id: string;
86
+ source: TaskEvaluationScenario["source"];
87
+ inputDigestSha256: string;
88
+ matched: boolean;
89
+ blocked: boolean;
90
+ sandboxActionCount: number;
91
+ criteria: Array<{
92
+ id: string;
93
+ status: "PASSED" | "FAILED" | "NOT_OBSERVED";
94
+ evaluator: "DETERMINISTIC" | "READ_ONLY_PROBE";
95
+ }>;
96
+ differences: string[];
97
+ }>;
98
+ limitations: string[];
99
+ }
100
+ export declare function runReplayEvaluation(options: {
101
+ task: TaskEvaluationContract;
102
+ harness: SandboxCertificationHarness;
103
+ tenantId: string;
104
+ scenarios: TaskEvaluationScenario[];
105
+ evaluateCandidate: (scenario: {
106
+ id: string;
107
+ source: TaskEvaluationScenario["source"];
108
+ input: unknown;
109
+ }) => Promise<TaskCandidateEvaluation>;
110
+ now?: () => Date;
111
+ }): Promise<TaskEvaluationReport>;
112
+ export declare function runShadowEvaluation(options: {
113
+ task: TaskEvaluationContract;
114
+ observations: TaskEvaluationScenario[];
115
+ evaluateCandidate: (context: {
116
+ id: string;
117
+ source: "LIVE_SHADOW";
118
+ input: unknown;
119
+ propose: (intent: Omit<TaskEvaluationIntent, "action">) => void;
120
+ }) => Promise<ShadowCandidateEvaluation>;
121
+ now?: () => Date;
122
+ }): Promise<TaskEvaluationReport>;
123
+ //# sourceMappingURL=task-evaluation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-evaluation.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/task-evaluation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,206 @@
1
+ export const TASK_EVALUATION_SCHEMA_VERSION = "witnora.task_evaluation.v0.1";
2
+ export async function runReplayEvaluation(options) {
3
+ validateTask(options.task);
4
+ validateScenarios(options.scenarios, false);
5
+ const now = options.now ?? (() => new Date());
6
+ const run = await options.harness.startRun({ tenantId: options.tenantId });
7
+ const results = [];
8
+ for (const scenario of options.scenarios) {
9
+ await options.harness.resetTenant(options.tenantId);
10
+ const candidate = await options.evaluateCandidate({ id: scenario.id, source: scenario.source, input: scenario.input });
11
+ validateCandidate(candidate, options.task);
12
+ const differences = compareCandidate(scenario, candidate);
13
+ let blocked = false;
14
+ let sandboxActionCount = 0;
15
+ for (const intent of candidate.actionIntents) {
16
+ const path = options.task.actionPaths.find((item) => item.id === intent.pathId);
17
+ if (!path || !intent.action || !actionMatchesPath(intent.action, path)) {
18
+ blocked = true;
19
+ differences.push(`Action path ${intent.pathId} was not bound to the approved sandbox contract.`);
20
+ continue;
21
+ }
22
+ const execution = await run.executeAction(intent.action, {
23
+ approval: { approved: true, reviewerId: `business-task:${options.task.id}`, comment: "Approved synthetic Replay path from the customer Business Task Contract." },
24
+ rollbackAfterVerification: true,
25
+ rollbackReason: "Replay restores the synthetic tenant after each observed action.",
26
+ });
27
+ if (execution.status !== "verified" && execution.status !== "rolled_back") {
28
+ blocked = true;
29
+ differences.push(`Sandbox action ${intent.pathId} was ${execution.status}${execution.rejectionCode ? ` (${execution.rejectionCode})` : ""}.`);
30
+ }
31
+ else {
32
+ sandboxActionCount += 1;
33
+ }
34
+ }
35
+ const criteria = options.task.successCriteria.map((criterion) => ({
36
+ id: criterion.id,
37
+ evaluator: criterion.evaluator,
38
+ status: candidate.criteria.find((item) => item.id === criterion.id)?.passed === true ? "PASSED" : "FAILED",
39
+ }));
40
+ if (criteria.some((item) => item.status !== "PASSED"))
41
+ differences.push("One or more declared success criteria did not pass.");
42
+ const matched = !blocked && differences.length === 0;
43
+ results.push({
44
+ id: scenario.id,
45
+ source: scenario.source,
46
+ inputDigestSha256: scenario.inputDigestSha256,
47
+ matched,
48
+ blocked,
49
+ sandboxActionCount,
50
+ criteria,
51
+ differences: [...new Set(differences)],
52
+ });
53
+ }
54
+ run.complete();
55
+ return report("REPLAY", "REPLAY_SANDBOX_ONLY", options.task, results, now());
56
+ }
57
+ export async function runShadowEvaluation(options) {
58
+ validateTask(options.task);
59
+ validateScenarios(options.observations, true);
60
+ const now = options.now ?? (() => new Date());
61
+ const results = [];
62
+ for (const observation of options.observations) {
63
+ const proposals = [];
64
+ const candidate = await options.evaluateCandidate({
65
+ id: observation.id,
66
+ source: "LIVE_SHADOW",
67
+ input: observation.input,
68
+ propose: (intent) => {
69
+ digest(intent.parametersDigestSha256, `candidate action ${intent.pathId} digest`);
70
+ if (!options.task.actionPaths.some((path) => path.id === intent.pathId)) {
71
+ throw new Error(`Candidate proposed undeclared action path ${intent.pathId}.`);
72
+ }
73
+ proposals.push(structuredClone(intent));
74
+ },
75
+ });
76
+ validateShadowCandidate(candidate, options.task);
77
+ const candidateForComparison = {
78
+ resultDigestSha256: candidate.resultDigestSha256,
79
+ actionIntents: proposals,
80
+ criteria: candidate.criteria,
81
+ };
82
+ const differences = compareCandidate(observation, candidateForComparison);
83
+ const criteria = options.task.successCriteria.map((criterion) => ({
84
+ id: criterion.id,
85
+ evaluator: criterion.evaluator,
86
+ status: criterion.evaluator === "READ_ONLY_PROBE"
87
+ ? "NOT_OBSERVED"
88
+ : candidate.criteria.find((item) => item.id === criterion.id)?.passed === true
89
+ ? "PASSED"
90
+ : "FAILED",
91
+ }));
92
+ if (criteria.some((item) => item.status === "FAILED"))
93
+ differences.push("One or more deterministic success criteria did not pass.");
94
+ results.push({
95
+ id: observation.id,
96
+ source: observation.source,
97
+ inputDigestSha256: observation.inputDigestSha256,
98
+ matched: differences.length === 0,
99
+ blocked: false,
100
+ sandboxActionCount: 0,
101
+ criteria,
102
+ differences: [...new Set(differences)],
103
+ });
104
+ }
105
+ return report("SHADOW", "SHADOW_NO_WRITE", options.task, results, now());
106
+ }
107
+ function report(mode, classification, task, scenarios, generatedAt) {
108
+ const blocked = scenarios.filter((item) => item.blocked).length;
109
+ const matched = scenarios.filter((item) => item.matched).length;
110
+ const changed = scenarios.length - matched - blocked;
111
+ return {
112
+ schemaVersion: TASK_EVALUATION_SCHEMA_VERSION,
113
+ mode,
114
+ classification,
115
+ generatedAt: generatedAt.toISOString(),
116
+ task: {
117
+ id: task.id,
118
+ digestSha256: task.digestSha256,
119
+ taskKey: task.taskKey,
120
+ version: task.version,
121
+ subject: structuredClone(task.subject),
122
+ scenarioSuite: structuredClone(task.scenarioSuite),
123
+ },
124
+ verdict: blocked ? "BLOCKED" : changed ? "CHANGED" : mode === "SHADOW" ? "MATCHED" : "PASSED",
125
+ summary: {
126
+ scenarios: scenarios.length,
127
+ matched,
128
+ changed,
129
+ blocked,
130
+ sandboxActions: scenarios.reduce((sum, item) => sum + item.sandboxActionCount, 0),
131
+ productionWrites: 0,
132
+ },
133
+ scenarios,
134
+ limitations: mode === "REPLAY"
135
+ ? [
136
+ "Replay covers only the declared scenarios and network-denied synthetic or sanitized sandbox paths.",
137
+ "Replay results do not establish production enforcement, production outcome verification, or future Agent reliability.",
138
+ ]
139
+ : [
140
+ "Shadow compares candidate decisions against observed inputs while candidate writes remain blocked.",
141
+ "Shadow results do not establish that the candidate produced a real production outcome.",
142
+ ],
143
+ };
144
+ }
145
+ function compareCandidate(scenario, candidate) {
146
+ const differences = [];
147
+ if (candidate.resultDigestSha256 !== scenario.baseline.resultDigestSha256)
148
+ differences.push("Candidate result digest differs from the baseline.");
149
+ const expected = scenario.baseline.actionIntents.map(intentFingerprint).sort();
150
+ const observed = candidate.actionIntents.map(intentFingerprint).sort();
151
+ if (JSON.stringify(expected) !== JSON.stringify(observed))
152
+ differences.push("Candidate action intents differ from the baseline.");
153
+ return differences;
154
+ }
155
+ function intentFingerprint(intent) {
156
+ return `${intent.pathId}:${intent.parametersDigestSha256}`;
157
+ }
158
+ function actionMatchesPath(action, path) {
159
+ const permissions = [...new Set(action.requestedPermissions ?? [])].sort();
160
+ return path.customerDecision !== "DENY"
161
+ && action.environment !== "production"
162
+ && !action.targetUrl
163
+ && action.actionType === path.actionType
164
+ && action.targetSystem === path.targetSystem
165
+ && JSON.stringify(permissions) === JSON.stringify([...new Set(path.requiredPermissions)].sort());
166
+ }
167
+ function validateTask(task) {
168
+ digest(task.digestSha256, "task.digestSha256");
169
+ digest(task.scenarioSuite.sha256, "task.scenarioSuite.sha256");
170
+ if (!task.actionPaths.length || !task.successCriteria.length)
171
+ throw new Error("Task evaluation requires action paths and success criteria.");
172
+ for (const criterion of task.successCriteria)
173
+ digest(criterion.predicateDigestSha256, `criterion ${criterion.id}`);
174
+ }
175
+ function validateScenarios(scenarios, shadow) {
176
+ if (!scenarios.length || scenarios.length > 500)
177
+ throw new Error("Task evaluation requires 1 to 500 scenarios.");
178
+ for (const scenario of scenarios) {
179
+ if (!shadow && scenario.source === "LIVE_SHADOW")
180
+ throw new Error("Replay cannot consume a LIVE_SHADOW scenario.");
181
+ if (shadow && scenario.source !== "LIVE_SHADOW")
182
+ throw new Error("Shadow requires LIVE_SHADOW observations.");
183
+ if (scenario.sanitized !== true)
184
+ throw new Error(`Scenario ${scenario.id} must be explicitly sanitized.`);
185
+ digest(scenario.inputDigestSha256, `scenario ${scenario.id} input digest`);
186
+ digest(scenario.baseline.resultDigestSha256, `scenario ${scenario.id} baseline result digest`);
187
+ scenario.baseline.actionIntents.forEach((intent) => digest(intent.parametersDigestSha256, `scenario ${scenario.id} baseline action digest`));
188
+ }
189
+ }
190
+ function validateShadowCandidate(candidate, task) {
191
+ digest(candidate.resultDigestSha256, "candidate result digest");
192
+ const criterionIds = new Set(task.successCriteria.map((item) => item.id));
193
+ if (candidate.criteria.some((item) => !criterionIds.has(item.id)))
194
+ throw new Error("Candidate returned an undeclared success criterion.");
195
+ }
196
+ function validateCandidate(candidate, task) {
197
+ digest(candidate.resultDigestSha256, "candidate result digest");
198
+ candidate.actionIntents.forEach((intent) => digest(intent.parametersDigestSha256, `candidate action ${intent.pathId} digest`));
199
+ const criterionIds = new Set(task.successCriteria.map((item) => item.id));
200
+ if (candidate.criteria.some((item) => !criterionIds.has(item.id)))
201
+ throw new Error("Candidate returned an undeclared success criterion.");
202
+ }
203
+ function digest(value, name) {
204
+ if (!/^[a-f0-9]{64}$/.test(value))
205
+ throw new Error(`${name} must be a lowercase SHA-256 digest.`);
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.13.7",
3
+ "version": "0.13.8",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",