witnora 0.13.8 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Witnora CLI
2
2
 
3
+ ## Customer Evaluator Kit
4
+
5
+ `witnora/evaluator-kit` exports `createBusinessTaskEvaluatorServer()` for the customer-side
6
+ Replay and Shadow evaluator consumed by the managed Workflow Harness. It listens only on a
7
+ literal loopback address, requires a pinned bearer credential and contract digest, validates
8
+ the exact Business Task binding, and returns only the structured evaluation report. Customer
9
+ scenario inputs and candidate callbacks stay in the customer process. Replay can execute only
10
+ through the supplied synthetic sandbox harness; Shadow exposes no write interface.
11
+
3
12
  Unified release assurance, evidence, corpus, monitor, and lab CLI for Witnora.
4
13
 
5
14
  Witnora checks what an agent may do, whether it passed pre-release evidence,
package/dist/gateway.js CHANGED
@@ -578,7 +578,8 @@ export async function createConfiguredWorkflowHarness(input) {
578
578
  const healthBody = await health.json().catch(() => ({}));
579
579
  if (!health.ok
580
580
  || healthBody.schemaVersion !== "witnora.business_task_evaluator.v0.1"
581
- || healthBody.contractSha256 !== input.config.evaluatorContractSha256) {
581
+ || healthBody.contractSha256 !== input.config.evaluatorContractSha256
582
+ || healthBody.ready !== true) {
582
583
  throw new Error("Managed Workflow evaluator health did not match its pinned local contract.");
583
584
  }
584
585
  const hosted = input.managed.createManagedWorkflowHostedClient({
@@ -0,0 +1,49 @@
1
+ import type { SandboxCertificationHarness } from "./sandbox-harness.js";
2
+ import { type ShadowCandidateEvaluation, type TaskCandidateEvaluation, type TaskEvaluationContract, type TaskEvaluationScenario } from "./task-evaluation.js";
3
+ export declare const BUSINESS_TASK_EVALUATOR_SCHEMA_VERSION: "witnora.business_task_evaluator.v0.1";
4
+ export declare const BUSINESS_TASK_EVALUATION_REQUEST_SCHEMA_VERSION: "witnora.business_task_evaluation_request.v0.1";
5
+ export interface BusinessTaskEvaluationBinding {
6
+ nodeId: string;
7
+ taskContractId: string;
8
+ taskContractDigestSha256: string;
9
+ taskKey: string;
10
+ agentId: string;
11
+ agentVersion: string;
12
+ mode: "REPLAY" | "SHADOW";
13
+ }
14
+ export interface BusinessTaskEvaluatorServerOptions {
15
+ credential: string;
16
+ contractSha256: string;
17
+ host?: "127.0.0.1" | "::1";
18
+ port?: number;
19
+ maxBodyBytes?: number;
20
+ maxConcurrency?: number;
21
+ evaluationTimeoutMs?: number;
22
+ loadReplayScenarios?(task: TaskEvaluationContract, binding: BusinessTaskEvaluationBinding, signal: AbortSignal): Promise<TaskEvaluationScenario[]>;
23
+ createReplayHarness?(task: TaskEvaluationContract, binding: BusinessTaskEvaluationBinding, signal: AbortSignal): Promise<{
24
+ harness: SandboxCertificationHarness;
25
+ tenantId: string;
26
+ }>;
27
+ evaluateReplayCandidate?(scenario: {
28
+ id: string;
29
+ source: TaskEvaluationScenario["source"];
30
+ input: unknown;
31
+ }, task: TaskEvaluationContract, binding: BusinessTaskEvaluationBinding, signal: AbortSignal): Promise<TaskCandidateEvaluation>;
32
+ loadShadowObservations?(task: TaskEvaluationContract, binding: BusinessTaskEvaluationBinding, signal: AbortSignal): Promise<TaskEvaluationScenario[]>;
33
+ evaluateShadowCandidate?(context: {
34
+ id: string;
35
+ source: "LIVE_SHADOW";
36
+ input: unknown;
37
+ propose: (intent: {
38
+ pathId: string;
39
+ parametersDigestSha256: string;
40
+ }) => void;
41
+ }, task: TaskEvaluationContract, binding: BusinessTaskEvaluationBinding, signal: AbortSignal): Promise<ShadowCandidateEvaluation>;
42
+ }
43
+ export declare function createBusinessTaskEvaluatorServer(options: BusinessTaskEvaluatorServerOptions): {
44
+ start(): Promise<{
45
+ origin: string;
46
+ }>;
47
+ close(): Promise<void>;
48
+ };
49
+ //# sourceMappingURL=business-task-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"business-task-evaluator.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/business-task-evaluator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAE3B,KAAK,sBAAsB,EAC5B,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,sCAAsC,EAAG,sCAA+C,CAAC;AACtG,eAAO,MAAM,+CAA+C,EAAG,+CAAwD,CAAC;AAExH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,kCAAkC;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,CAAC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACnJ,mBAAmB,CAAC,CAAC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,2BAA2B,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrL,uBAAuB,CAAC,CAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC1O,sBAAsB,CAAC,CAAC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACtJ,uBAAuB,CAAC,CAAC,OAAO,EAAE;QAChC,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,aAAa,CAAC;QACtB,KAAK,EAAE,OAAO,CAAC;QACf,OAAO,EAAE,CAAC,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,sBAAsB,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;KAC/E,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACnI;AAED,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,kCAAkC;aA+C1E,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;aAQ3B,OAAO,CAAC,IAAI,CAAC;EAK/B"}
@@ -0,0 +1,185 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { runReplayEvaluation, runShadowEvaluation, } from "./task-evaluation.js";
4
+ export const BUSINESS_TASK_EVALUATOR_SCHEMA_VERSION = "witnora.business_task_evaluator.v0.1";
5
+ export const BUSINESS_TASK_EVALUATION_REQUEST_SCHEMA_VERSION = "witnora.business_task_evaluation_request.v0.1";
6
+ export function createBusinessTaskEvaluatorServer(options) {
7
+ const host = options.host ?? "127.0.0.1";
8
+ if (host !== "127.0.0.1" && host !== "::1")
9
+ throw new Error("Business Task Evaluator accepts only a literal loopback host.");
10
+ const credential = required(options.credential, "credential");
11
+ const contractSha256 = digest(options.contractSha256, "contractSha256");
12
+ const port = bounded(options.port ?? 0, 0, 65_535, "port");
13
+ const maxBodyBytes = bounded(options.maxBodyBytes ?? 1_048_576, 1_024, 4_194_304, "maxBodyBytes");
14
+ const maxConcurrency = bounded(options.maxConcurrency ?? 4, 1, 16, "maxConcurrency");
15
+ const evaluationTimeoutMs = bounded(options.evaluationTimeoutMs ?? 120_000, 1_000, 600_000, "evaluationTimeoutMs");
16
+ const modes = [
17
+ ...(options.loadReplayScenarios && options.createReplayHarness && options.evaluateReplayCandidate ? ["REPLAY"] : []),
18
+ ...(options.loadShadowObservations && options.evaluateShadowCandidate ? ["SHADOW"] : []),
19
+ ];
20
+ let active = 0;
21
+ const server = createServer(async (request, response) => {
22
+ response.setHeader("cache-control", "no-store");
23
+ response.setHeader("content-type", "application/json; charset=utf-8");
24
+ try {
25
+ if (!authorized(request, credential))
26
+ return send(response, 401, { error: "unauthorized" });
27
+ if (request.method === "GET" && request.url === "/healthz") {
28
+ return send(response, 200, {
29
+ schemaVersion: BUSINESS_TASK_EVALUATOR_SCHEMA_VERSION,
30
+ contractSha256,
31
+ ready: modes.length > 0,
32
+ modes,
33
+ boundary: { host, productionWrites: 0, rawInputsRetainedByCustomer: true },
34
+ });
35
+ }
36
+ if (request.method !== "POST" || request.url !== "/v1/evaluate")
37
+ return send(response, 404, { error: "not_found" });
38
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
39
+ return send(response, 415, { error: "json_required" });
40
+ }
41
+ if (active >= maxConcurrency)
42
+ return send(response, 429, { error: "evaluator_busy" });
43
+ const body = await readJson(request, maxBodyBytes);
44
+ const parsed = parseRequest(body);
45
+ validateBinding(parsed.task, parsed.evaluation);
46
+ active += 1;
47
+ try {
48
+ const report = await withTimeout((signal) => evaluate(options, parsed.task, parsed.evaluation, signal), evaluationTimeoutMs);
49
+ return send(response, 200, { schemaVersion: BUSINESS_TASK_EVALUATOR_SCHEMA_VERSION, report });
50
+ }
51
+ finally {
52
+ active -= 1;
53
+ }
54
+ }
55
+ catch (error) {
56
+ const status = error instanceof EvaluatorError ? error.status : 422;
57
+ return send(response, status, { error: error instanceof EvaluatorError ? error.code : "evaluation_failed" });
58
+ }
59
+ });
60
+ return {
61
+ async start() {
62
+ if (!server.listening)
63
+ await new Promise((resolve, reject) => {
64
+ server.once("error", reject);
65
+ server.listen(port, host, () => { server.off("error", reject); resolve(); });
66
+ });
67
+ const address = server.address();
68
+ return { origin: `http://${host === "::1" ? "[::1]" : host}:${address.port}` };
69
+ },
70
+ async close() {
71
+ if (!server.listening)
72
+ return;
73
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
74
+ },
75
+ };
76
+ }
77
+ async function evaluate(options, task, binding, signal) {
78
+ if (binding.mode === "REPLAY") {
79
+ if (!options.loadReplayScenarios || !options.createReplayHarness || !options.evaluateReplayCandidate) {
80
+ throw new EvaluatorError(409, "replay_not_configured");
81
+ }
82
+ const [{ harness, tenantId }, scenarios] = await Promise.all([
83
+ options.createReplayHarness(task, binding, signal), options.loadReplayScenarios(task, binding, signal),
84
+ ]);
85
+ return runReplayEvaluation({
86
+ task, harness, tenantId, scenarios,
87
+ evaluateCandidate: (scenario) => options.evaluateReplayCandidate(scenario, task, binding, signal),
88
+ });
89
+ }
90
+ if (!options.loadShadowObservations || !options.evaluateShadowCandidate)
91
+ throw new EvaluatorError(409, "shadow_not_configured");
92
+ return runShadowEvaluation({
93
+ task,
94
+ observations: await options.loadShadowObservations(task, binding, signal),
95
+ evaluateCandidate: (context) => options.evaluateShadowCandidate(context, task, binding, signal),
96
+ });
97
+ }
98
+ function parseRequest(input) {
99
+ const body = object(input, "request");
100
+ exactKeys(body, ["schemaVersion", "task", "evaluation"], "request");
101
+ if (body.schemaVersion !== BUSINESS_TASK_EVALUATION_REQUEST_SCHEMA_VERSION)
102
+ throw new EvaluatorError(422, "invalid_schema");
103
+ const evaluation = object(body.evaluation, "evaluation");
104
+ exactKeys(evaluation, ["nodeId", "taskContractId", "taskContractDigestSha256", "taskKey", "agentId", "agentVersion", "mode"], "evaluation");
105
+ if (evaluation.mode !== "REPLAY" && evaluation.mode !== "SHADOW")
106
+ throw new EvaluatorError(422, "invalid_mode");
107
+ const binding = {
108
+ nodeId: requiredString(evaluation.nodeId), taskContractId: requiredString(evaluation.taskContractId),
109
+ taskContractDigestSha256: digest(String(evaluation.taskContractDigestSha256 ?? ""), "taskContractDigestSha256"),
110
+ taskKey: requiredString(evaluation.taskKey), agentId: requiredString(evaluation.agentId),
111
+ agentVersion: requiredString(evaluation.agentVersion), mode: evaluation.mode,
112
+ };
113
+ return { task: object(body.task, "task"), evaluation: binding };
114
+ }
115
+ function validateBinding(task, binding) {
116
+ if (task.id !== binding.taskContractId || task.digestSha256 !== binding.taskContractDigestSha256
117
+ || task.taskKey !== binding.taskKey || task.subject?.agentId !== binding.agentId
118
+ || task.subject?.agentVersion !== binding.agentVersion)
119
+ throw new EvaluatorError(409, "task_binding_mismatch");
120
+ }
121
+ function authorized(request, credential) {
122
+ const supplied = String(request.headers.authorization ?? "").replace(/^Bearer\s+/i, "");
123
+ const left = Buffer.from(supplied);
124
+ const right = Buffer.from(credential);
125
+ return left.length === right.length && left.length > 0 && timingSafeEqual(left, right);
126
+ }
127
+ async function readJson(request, limit) {
128
+ const chunks = [];
129
+ let size = 0;
130
+ for await (const chunk of request) {
131
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
132
+ size += value.length;
133
+ if (size > limit)
134
+ throw new EvaluatorError(413, "body_too_large");
135
+ chunks.push(value);
136
+ }
137
+ try {
138
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
139
+ }
140
+ catch {
141
+ throw new EvaluatorError(400, "invalid_json");
142
+ }
143
+ }
144
+ async function withTimeout(execute, timeoutMs) {
145
+ const controller = new AbortController();
146
+ let timer;
147
+ try {
148
+ return await Promise.race([execute(controller.signal), new Promise((_, reject) => {
149
+ timer = setTimeout(() => { controller.abort(); reject(new EvaluatorError(504, "evaluation_timeout")); }, timeoutMs);
150
+ timer.unref?.();
151
+ })]);
152
+ }
153
+ finally {
154
+ if (timer)
155
+ clearTimeout(timer);
156
+ }
157
+ }
158
+ function send(response, status, body) { response.statusCode = status; response.end(JSON.stringify(body)); }
159
+ function object(value, name) {
160
+ if (!value || typeof value !== "object" || Array.isArray(value))
161
+ throw new EvaluatorError(422, `invalid_${name}`);
162
+ return value;
163
+ }
164
+ function exactKeys(value, allowed, name) {
165
+ const keys = new Set(allowed);
166
+ if (Object.keys(value).some((key) => !keys.has(key)))
167
+ throw new EvaluatorError(422, `unexpected_${name}_field`);
168
+ }
169
+ function required(value, name) { const result = value.trim(); if (!result)
170
+ throw new Error(`${name} is required.`); return result; }
171
+ function requiredString(value) { if (typeof value !== "string" || !value.trim())
172
+ throw new EvaluatorError(422, "invalid_binding"); return value.trim(); }
173
+ function digest(value, name) { if (!/^[a-f0-9]{64}$/.test(value))
174
+ throw new Error(`${name} must be a lowercase SHA-256 digest.`); return value; }
175
+ function bounded(value, min, max, name) { if (!Number.isInteger(value) || value < min || value > max)
176
+ throw new Error(`${name} must be between ${min} and ${max}.`); return value; }
177
+ class EvaluatorError extends Error {
178
+ status;
179
+ code;
180
+ constructor(status, code) {
181
+ super(code);
182
+ this.status = status;
183
+ this.code = code;
184
+ }
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.13.8",
3
+ "version": "0.13.9",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/vendor/onegent-runtime/browser-adapter-kit.d.ts",
41
41
  "import": "./dist/vendor/onegent-runtime/browser-adapter-kit.js"
42
42
  },
43
+ "./evaluator-kit": {
44
+ "types": "./dist/vendor/onegent-runtime/business-task-evaluator.d.ts",
45
+ "import": "./dist/vendor/onegent-runtime/business-task-evaluator.js"
46
+ },
43
47
  "./deployment-enforcement": {
44
48
  "types": "./dist/internal/control-client/deployment-enforcement.d.ts",
45
49
  "import": "./dist/internal/control-client/deployment-enforcement.js"