jev-layer 0.1.0

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CONTRIBUTING.md +146 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/README.ru.md +117 -0
  6. package/README.zh-CN.md +117 -0
  7. package/RELEASE.md +53 -0
  8. package/SECURITY.md +56 -0
  9. package/bin/jev.mjs +214 -0
  10. package/config/codex.mcp.toml +8 -0
  11. package/config/generic-mcp.json +13 -0
  12. package/config/hermes.mcp.yaml +9 -0
  13. package/config/jev.example.json +22 -0
  14. package/config/omp.mcp.json +14 -0
  15. package/config/providers.env.example +13 -0
  16. package/docs/COMPATIBILITY.md +15 -0
  17. package/docs/SCHEMA-VERSIONING.md +94 -0
  18. package/examples/capabilities.json +39 -0
  19. package/examples/route-request.json +28 -0
  20. package/integrations/codex/.codex-plugin/plugin.json +19 -0
  21. package/integrations/codex/.mcp.json +11 -0
  22. package/integrations/codex/AGENTS.md +1 -0
  23. package/integrations/codex/run-mcp.mjs +9 -0
  24. package/integrations/codex/skills/jev-route/SKILL.md +17 -0
  25. package/integrations/hermes/__init__.py +51 -0
  26. package/integrations/hermes/plugin.yaml +5 -0
  27. package/integrations/hermes/schemas.py +12 -0
  28. package/integrations/omp/extension.js +105 -0
  29. package/integrations/template/README.md +10 -0
  30. package/integrations/template/adapter.mjs +87 -0
  31. package/package.json +59 -0
  32. package/scripts/benchmark.mjs +35 -0
  33. package/scripts/browser-benchmark.mjs +92 -0
  34. package/scripts/browser-e2e.mjs +117 -0
  35. package/scripts/capability-e2e.mjs +31 -0
  36. package/scripts/clean-install-smoke.mjs +162 -0
  37. package/scripts/codex-mcp-smoke.mjs +119 -0
  38. package/scripts/context-filter-e2e.mjs +45 -0
  39. package/scripts/fail-open-smoke.mjs +120 -0
  40. package/scripts/feature-flags-smoke.mjs +46 -0
  41. package/scripts/mcp-receipt-smoke.mjs +98 -0
  42. package/scripts/openrouter-choice.mjs +59 -0
  43. package/scripts/replay-eval.mjs +124 -0
  44. package/scripts/smoke.mjs +34 -0
  45. package/scripts/supervision-e2e.mjs +109 -0
  46. package/src/browser.mjs +569 -0
  47. package/src/cli.mjs +33 -0
  48. package/src/config.mjs +75 -0
  49. package/src/context-filter.mjs +56 -0
  50. package/src/contract.mjs +72 -0
  51. package/src/discovery.mjs +65 -0
  52. package/src/mcp-server.mjs +210 -0
  53. package/src/providers/demo.mjs +52 -0
  54. package/src/providers/typesafe.mjs +126 -0
  55. package/src/receipts.mjs +226 -0
  56. package/src/registry.mjs +109 -0
  57. package/src/relevance-filter.mjs +99 -0
  58. package/src/route.mjs +221 -0
  59. package/src/supervision.mjs +244 -0
  60. package/test/browser.test.mjs +199 -0
  61. package/test/capability.test.mjs +54 -0
  62. package/test/context-filter.test.mjs +65 -0
  63. package/test/openrouter-provider.test.mjs +55 -0
  64. package/test/receipts.test.mjs +71 -0
  65. package/test/route.test.mjs +74 -0
  66. package/test/supervision.test.mjs +99 -0
@@ -0,0 +1,12 @@
1
+ ROUTE = {
2
+ "type": "object",
3
+ "required": ["intent", "capabilities"],
4
+ "properties": {
5
+ "intent": {"type": "string"},
6
+ "harness": {"type": "string"},
7
+ "context": {"type": "object"},
8
+ "actor_permissions": {"type": "array", "items": {"type": "string"}},
9
+ "capabilities": {"type": "array", "items": {"type": "object"}},
10
+ "policy": {"type": "object"},
11
+ },
12
+ }
@@ -0,0 +1,105 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const PACKAGE_ROOT = process.env.JEV_LAYER_ROOT
6
+ ? resolve(process.env.JEV_LAYER_ROOT)
7
+ : fileURLToPath(new URL("../..", import.meta.url));
8
+ const CORE = resolve(PACKAGE_ROOT, "src/cli.mjs");
9
+ export default function jevLayerExtension(pi) {
10
+ const z = pi.zod;
11
+ pi.registerTool({
12
+ name: "jev_route",
13
+ label: "Jev route",
14
+ description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
15
+ parameters: z.object({
16
+ intent: z.string(),
17
+ harness: z.string().optional(),
18
+ context: z.unknown().optional(),
19
+ actor_permissions: z.array(z.string()).optional(),
20
+ capabilities: z.array(z.unknown()),
21
+ policy: z.unknown().optional(),
22
+ }),
23
+ async execute(_toolCallId, params, signal) {
24
+ const decision = await runCore({ ...params, harness: params.harness ?? "omp" }, signal);
25
+ return {
26
+ content: [{ type: "text", text: JSON.stringify(decision) }],
27
+ details: decision,
28
+ };
29
+ },
30
+ });
31
+ }
32
+
33
+ async function runCore(payload, signal) {
34
+ if (globalThis.Bun?.spawn) return runCoreWithBun(payload, signal);
35
+ return runCoreWithNode(payload, signal);
36
+ }
37
+
38
+ async function runCoreWithBun(payload, signal) {
39
+ const child = Bun.spawn([process.env.JEV_NODE ?? "node", CORE, "--provider", process.env.JEV_LAYER_PROVIDER ?? "demo"], {
40
+ cwd: process.cwd(),
41
+ env: process.env,
42
+ stdin: "pipe",
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ });
46
+ const abort = () => child.kill("SIGTERM");
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
49
+ child.stdin.end();
50
+ const [stdout, stderr, code] = await Promise.all([
51
+ new Response(child.stdout).text(),
52
+ new Response(child.stderr).text(),
53
+ child.exited,
54
+ ]);
55
+ signal?.removeEventListener("abort", abort);
56
+ return parseCoreOutput(stdout, stderr, code);
57
+ }
58
+
59
+ function runCoreWithNode(payload, signal) {
60
+ return new Promise((resolve) => {
61
+ const child = spawn(process.env.JEV_NODE ?? "node", [CORE, "--provider", process.env.JEV_LAYER_PROVIDER ?? "demo"], {
62
+ cwd: process.cwd(),
63
+ env: process.env,
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ let stdout = "";
67
+ let stderr = "";
68
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
69
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
70
+ const abort = () => child.kill("SIGTERM");
71
+ signal?.addEventListener("abort", abort, { once: true });
72
+ child.on("error", (error) => resolve(fallback(error.message)));
73
+ child.on("close", (code) => {
74
+ signal?.removeEventListener("abort", abort);
75
+ resolve(parseCoreOutput(stdout, stderr, code));
76
+ });
77
+ child.stdin.end(`${JSON.stringify(payload)}\n`);
78
+ });
79
+ }
80
+
81
+ function parseCoreOutput(stdout, stderr, code) {
82
+ if (code !== 0) return fallback(stderr.trim() || `core exited with status ${code}`);
83
+ const lines = stdout.split(/\r?\n/);
84
+ for (const line of lines) {
85
+ const jsonStart = line.indexOf("{");
86
+ if (jsonStart < 0) continue;
87
+ try {
88
+ return JSON.parse(line.slice(jsonStart));
89
+ } catch {
90
+ // Ignore non-decision lines emitted by a host runtime wrapper.
91
+ }
92
+ }
93
+ return fallback("core returned no valid JSON decision");
94
+ }
95
+
96
+ function fallback(reason) {
97
+ return {
98
+ schema_version: 1,
99
+ status: "fallback",
100
+ selected: null,
101
+ reason,
102
+ fallback: { type: "adapter_error", reason },
103
+ execution: { enabled: false, status: "not_started" },
104
+ };
105
+ }
@@ -0,0 +1,10 @@
1
+ # Template harness adapter
2
+
3
+ Copy `adapter.mjs` to `integrations/<harness>/` and replace the injected callbacks with the harness's native implementations:
4
+
5
+ - `jevRoute(request)`: call `jev_route` through the existing stdio MCP or CLI boundary;
6
+ - `jevRecordExecution(args)`: call `jev_record_execution` with the original `correlation_id`;
7
+ - `approve(capabilityId, decision, request)`: delegate to the host's normal approval UI/policy;
8
+ - `executeNative(capabilityId, request, decision)`: resolve and execute the id through the host registry.
9
+
10
+ The template intentionally does not discover arbitrary tools, grant permissions, retry actions, or spawn workers. Jev failures return `native_fallback`; the host should continue its normal path. Copy the template only after adding a harness-specific smoke fixture and compatibility entry.
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Copy this file to integrations/<your-harness>/adapter.mjs.
3
+ * The three injected functions are the only Jev/host boundaries:
4
+ * jevRoute(request) -> decision from jev_route
5
+ * jevRecordExecution(args) -> execution_receipt from jev_record_execution
6
+ * executeNative(capabilityId, request) -> host-owned result
7
+ */
8
+
9
+ export function createTemplateAdapter({ jevRoute, jevRecordExecution, executeNative, approve = async () => true }) {
10
+ for (const [name, value] of Object.entries({ jevRoute, jevRecordExecution, executeNative, approve })) {
11
+ if (typeof value !== "function") throw new TypeError(`${name} callback is required`);
12
+ }
13
+
14
+ return {
15
+ async run(request) {
16
+ let decision;
17
+ try {
18
+ decision = await jevRoute(request);
19
+ } catch (error) {
20
+ return nativeFallback("jev_unavailable", error);
21
+ }
22
+
23
+ if (decision?.status !== "selected" || typeof decision.selected !== "string" || decision.selected === "") {
24
+ return { status: "native_fallback", reason: decision?.reason ?? "no_selection", decision };
25
+ }
26
+
27
+ const approval = await approve(decision.selected, decision, request);
28
+ if (!approval) {
29
+ const receipt = await record(jevRecordExecution, {
30
+ correlation_id: decision.correlation_id,
31
+ harness: request.harness,
32
+ capability_id: decision.selected,
33
+ status: "not_started",
34
+ error: "host approval denied",
35
+ });
36
+ return { status: "native_fallback", reason: "host_confirmation_required", decision, receipt };
37
+ }
38
+
39
+ const started = Date.now();
40
+ let host;
41
+ try {
42
+ host = await executeNative(decision.selected, request, decision);
43
+ } catch (error) {
44
+ const receipt = await record(jevRecordExecution, {
45
+ correlation_id: decision.correlation_id,
46
+ harness: request.harness,
47
+ capability_id: decision.selected,
48
+ status: "failed",
49
+ error: boundedError(error),
50
+ duration_ms: Date.now() - started,
51
+ });
52
+ return { status: "native_fallback", reason: "host_execution_failed", decision, error: boundedError(error), receipt };
53
+ }
54
+
55
+ const receipt = await record(jevRecordExecution, {
56
+ correlation_id: decision.correlation_id,
57
+ harness: request.harness,
58
+ capability_id: decision.selected,
59
+ status: "completed",
60
+ result: host,
61
+ duration_ms: Date.now() - started,
62
+ });
63
+ return { status: "completed", decision, host, receipt };
64
+ },
65
+ };
66
+ }
67
+
68
+ async function record(jevRecordExecution, args) {
69
+ try {
70
+ return await jevRecordExecution(args);
71
+ } catch (error) {
72
+ // Execution ownership stays with the host even if receipt persistence fails.
73
+ return { persisted: false, error: boundedError(error) };
74
+ }
75
+ }
76
+
77
+ function nativeFallback(reason, error) {
78
+ return {
79
+ status: "native_fallback",
80
+ reason,
81
+ error: error ? boundedError(error) : null,
82
+ };
83
+ }
84
+
85
+ function boundedError(error) {
86
+ return String(error instanceof Error ? error.message : error).slice(0, 2_000);
87
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "jev-layer",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/typakon4/jev-layer.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/typakon4/jev-layer/issues"
11
+ },
12
+ "homepage": "https://github.com/typakon4/jev-layer#readme",
13
+ "type": "module",
14
+ "bin": {
15
+ "jev": "bin/jev.mjs"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "config/",
20
+ "docs/",
21
+ "examples/",
22
+ "integrations/",
23
+ "scripts/",
24
+ "src/",
25
+ "test/",
26
+ "CHANGELOG.md",
27
+ "CONTRIBUTING.md",
28
+ "LICENSE",
29
+ "README.md",
30
+ "README.ru.md",
31
+ "README.zh-CN.md",
32
+ "RELEASE.md",
33
+ "SECURITY.md",
34
+ "package.json",
35
+ "package-lock.json"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "scripts": {
41
+ "jev": "node bin/jev.mjs",
42
+ "test": "node --test test/*.test.mjs",
43
+ "smoke": "node scripts/smoke.mjs",
44
+ "benchmark": "node scripts/benchmark.mjs",
45
+ "openrouter:choice": "node scripts/openrouter-choice.mjs",
46
+ "mcp": "node src/mcp-server.mjs",
47
+ "fail-open-smoke": "node scripts/fail-open-smoke.mjs",
48
+ "codex:mcp-smoke": "node scripts/codex-mcp-smoke.mjs",
49
+ "replay:evaluate": "node scripts/replay-eval.mjs",
50
+ "receipt:mcp-smoke": "node scripts/mcp-receipt-smoke.mjs",
51
+ "browser:e2e": "node scripts/browser-e2e.mjs",
52
+ "browser:benchmark": "node scripts/browser-benchmark.mjs",
53
+ "supervision:e2e": "node scripts/supervision-e2e.mjs",
54
+ "context-filter:e2e": "node scripts/context-filter-e2e.mjs",
55
+ "capability:e2e": "node scripts/capability-e2e.mjs",
56
+ "feature-flags:smoke": "node scripts/feature-flags-smoke.mjs",
57
+ "clean-install-smoke": "node scripts/clean-install-smoke.mjs"
58
+ }
59
+ }
@@ -0,0 +1,35 @@
1
+ import { routeRequest } from "../src/route.mjs";
2
+
3
+ const capabilities = [
4
+ { id: "repo_search", kind: "mcp", name: "Repository search", description: "Search repository files and symbols", permissions: ["read"], risk: "low" },
5
+ { id: "browser_inspect", kind: "mcp", name: "Browser inspection", description: "Open and inspect webpages", permissions: ["browser"], risk: "medium" },
6
+ { id: "terminal_read", kind: "cli", name: "Read-only terminal", description: "Run read-only terminal commands", permissions: ["terminal:read"], risk: "low" },
7
+ { id: "file_write", kind: "tool", name: "File writer", description: "Modify repository files", permissions: ["write"], risk: "high" },
8
+ ];
9
+ const request = {
10
+ harness: "benchmark",
11
+ intent: "Search repository files for the routing implementation",
12
+ context: { workspace: "jev-layer" },
13
+ actor_permissions: ["read"],
14
+ capabilities,
15
+ };
16
+ const samples = [];
17
+ const started = performance.now();
18
+ for (let index = 0; index < 1_000; index += 1) {
19
+ const before = performance.now();
20
+ await routeRequest(request, { provider: "demo" });
21
+ samples.push(performance.now() - before);
22
+ }
23
+ samples.sort((left, right) => left - right);
24
+ const percentile = (p) => Number(samples[Math.min(samples.length - 1, Math.floor(samples.length * p))].toFixed(3));
25
+ console.log(JSON.stringify({
26
+ provider: "jev-demo",
27
+ iterations: samples.length,
28
+ total_ms: Number((performance.now() - started).toFixed(3)),
29
+ p50_ms: percentile(0.5),
30
+ p95_ms: percentile(0.95),
31
+ p99_ms: percentile(0.99),
32
+ provider_calls_per_route: 1,
33
+ host_execution_calls: 0,
34
+ paid_provider_calls: 0,
35
+ }));
@@ -0,0 +1,92 @@
1
+ import { runBrowserFastPath } from "../src/browser.mjs";
2
+
3
+ const iterations = Number(process.env.JEV_BROWSER_BENCHMARK_ITERATIONS ?? 25);
4
+ const baseline = [];
5
+ const jev = [];
6
+
7
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
8
+ baseline.push(await runBaseline());
9
+ jev.push(await runJev());
10
+ }
11
+
12
+ const report = {
13
+ ok: true,
14
+ iterations,
15
+ provider: "injected-jev-demo-choice",
16
+ baseline: summarize(baseline),
17
+ jev: summarize(jev),
18
+ delta: {
19
+ wall_time_reduction_percent: percent(mean(baseline, "wall_time_ms"), mean(jev, "wall_time_ms")),
20
+ main_model_turn_reduction: mean(baseline, "main_model_turns") - mean(jev, "main_model_turns"),
21
+ extra_jev_calls: mean(jev, "jev_calls") - mean(baseline, "jev_calls"),
22
+ browser_action_delta: mean(jev, "browser_actions") - mean(baseline, "browser_actions"),
23
+ failure_delta: mean(jev, "failures") - mean(baseline, "failures"),
24
+ },
25
+ };
26
+ console.log(JSON.stringify(report, null, 2));
27
+
28
+ async function runBaseline() {
29
+ const started = performance.now();
30
+ const actions = ["scroll", "switch_tab"];
31
+ for (const operation of actions) await Promise.resolve(operation);
32
+ return {
33
+ wall_time_ms: elapsed(started),
34
+ main_model_turns: 2,
35
+ jev_calls: 0,
36
+ browser_actions: actions.length,
37
+ failures: 0,
38
+ cost_usd: 0,
39
+ };
40
+ }
41
+
42
+ async function runJev() {
43
+ let index = 0;
44
+ const choices = ["browser:scroll:down", "browser:switch_tab:tab-2"];
45
+ const result = await runBrowserFastPath({
46
+ goal: "scroll down and switch to the other tab",
47
+ enabled: true,
48
+ provider: {
49
+ name: "browser-benchmark-provider",
50
+ async decide({ candidates }) {
51
+ const requested = choices[index++] ?? "browser:handoff";
52
+ const choice = candidates.some((candidate) => candidate.id === requested)
53
+ ? requested
54
+ : candidates.find((candidate) => candidate.id === "browser:handoff")?.id ?? candidates[0]?.id;
55
+ return {
56
+ answers: { tool: { type: "choice", choice, probabilities: Object.fromEntries(candidates.map((candidate) => [candidate.id, candidate.id === choice ? 1 : 0])), confidence: 1 } },
57
+ usage: { cost: 0.0001 },
58
+ };
59
+ },
60
+ },
61
+ executor: {
62
+ state: 0,
63
+ async observe() {
64
+ return this.state === 0
65
+ ? { scroll: { up: false, down: true }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] }
66
+ : { scroll: { up: false, down: false }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] };
67
+ },
68
+ async execute() {
69
+ this.state += 1;
70
+ return { observation: await this.observe(), handoff: this.state >= 2 };
71
+ },
72
+ },
73
+ mainModelTurns: 1,
74
+ });
75
+ return result.metrics;
76
+ }
77
+
78
+ function summarize(rows) {
79
+ return Object.fromEntries(["wall_time_ms", "main_model_turns", "jev_calls", "browser_actions", "failures", "cost_usd"].map((key) => [key, mean(rows, key)]));
80
+ }
81
+
82
+ function mean(rows, key) {
83
+ return Number((rows.reduce((sum, row) => sum + row[key], 0) / rows.length).toFixed(3));
84
+ }
85
+
86
+ function percent(before, after) {
87
+ return before ? Number((((before - after) / before) * 100).toFixed(2)) : 0;
88
+ }
89
+
90
+ function elapsed(started) {
91
+ return Number((performance.now() - started).toFixed(3));
92
+ }
@@ -0,0 +1,117 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { createInterface } from "node:readline";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ const casesPath = join(await mkdtemp(join(tmpdir(), "jev-browser-e2e-")), "cases.jsonl");
11
+ const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
12
+ cwd: root,
13
+ env: { ...process.env, JEV_BROWSER_FAST_PATH: "1", JEV_REPLAY_CASES: casesPath },
14
+ stdio: ["pipe", "pipe", "pipe"],
15
+ });
16
+ const lines = createInterface({ input: child.stdout });
17
+ const pending = new Map();
18
+ lines.on("line", (line) => {
19
+ if (!line.trim()) return;
20
+ const message = JSON.parse(line);
21
+ const waiter = pending.get(message.id);
22
+ if (waiter) {
23
+ pending.delete(message.id);
24
+ waiter(message);
25
+ }
26
+ });
27
+
28
+ try {
29
+ const initialized = await request(1, "initialize", {});
30
+ assert.equal(initialized.result.serverInfo.name, "jev-layer");
31
+ await notification("notifications/initialized", {});
32
+ const listed = await request(2, "tools/list", {});
33
+ assert.ok(listed.result.tools.some((tool) => tool.name === "jev_browser_step"));
34
+
35
+ const routed = await request(3, "tools/call", {
36
+ name: "jev_browser_step",
37
+ arguments: {
38
+ provider: process.env.JEV_BROWSER_PROVIDER ?? "demo",
39
+ enabled: true,
40
+ harness: "browser-e2e",
41
+ goal: "scroll the page downward",
42
+ observation: {
43
+ url: "https://example.test",
44
+ title: "Fixture",
45
+ visible_text: "Additional content is below the fold",
46
+ scroll: { up: false, down: true },
47
+ targets: [],
48
+ tabs: [],
49
+ },
50
+ policy: { min_confidence: 0 },
51
+ },
52
+ });
53
+ const decision = routed.result.structuredContent;
54
+ assert.equal(decision.status, "selected");
55
+ assert.equal(decision.browser_action.operation, "scroll");
56
+ assert.equal(decision.browser_action.target_id, null);
57
+
58
+ const started = performance.now();
59
+ const hostResult = { operation: decision.browser_action.operation, direction: "down", fixture: true };
60
+ const recorded = await request(4, "tools/call", {
61
+ name: "jev_record_execution",
62
+ arguments: {
63
+ correlation_id: decision.correlation_id,
64
+ harness: "browser-e2e",
65
+ capability_id: decision.selected,
66
+ status: "completed",
67
+ result: hostResult,
68
+ browser: { operation: "scroll", direction: "down", executed: true },
69
+ exit_status: 0,
70
+ duration_ms: Number((performance.now() - started).toFixed(3)),
71
+ },
72
+ });
73
+ const receipt = recorded.result.structuredContent;
74
+ assert.equal(receipt.correlation_id, decision.correlation_id);
75
+ assert.equal(receipt.host.browser.operation, "scroll");
76
+ assert.equal(receipt.host.exit_status, 0);
77
+
78
+ const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
79
+ assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
80
+ assert.equal(records[0].request.context.browser.goal, "scroll the page downward");
81
+ console.log(JSON.stringify({
82
+ ok: true,
83
+ cases_path: casesPath,
84
+ decision: {
85
+ correlation_id: decision.correlation_id,
86
+ selected: decision.selected,
87
+ operation: decision.browser_action.operation,
88
+ confidence: decision.confidence,
89
+ latency_ms: decision.receipt.latency_ms,
90
+ cost_usd: decision.receipt.cost_usd ?? null,
91
+ },
92
+ host_execution: { operation: hostResult.operation, exit_status: receipt.host.exit_status },
93
+ receipts: records.length,
94
+ }, null, 2));
95
+ } finally {
96
+ child.kill();
97
+ await new Promise((resolveExit) => child.once("close", resolveExit));
98
+ }
99
+
100
+ function request(id, method, params) {
101
+ return new Promise((resolveResponse, reject) => {
102
+ const timer = setTimeout(() => {
103
+ pending.delete(id);
104
+ reject(new Error(`MCP request timed out: ${method}`));
105
+ }, 15_000);
106
+ pending.set(id, (message) => {
107
+ clearTimeout(timer);
108
+ resolveResponse(message);
109
+ });
110
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
111
+ });
112
+ }
113
+
114
+ function notification(method, params) {
115
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
116
+ return new Promise((resolveNotification) => setImmediate(resolveNotification));
117
+ }
@@ -0,0 +1,31 @@
1
+ import assert from "node:assert/strict";
2
+ import { routeRequest } from "../src/route.mjs";
3
+
4
+ let providerCalls = 0;
5
+ const decision = await routeRequest({
6
+ harness: "capability-e2e",
7
+ intent: "use the verified model for a bounded decision",
8
+ context: { task: "model" },
9
+ policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
10
+ }, {
11
+ discovery: {
12
+ skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect context" }],
13
+ models: [{ id: "model:verified", name: "Verified model", description: "Make bounded decisions", verified: true, risk: "low", source: "e2e-manifest" }],
14
+ tools: [{ id: "tool:offline", name: "Offline tool", description: "Unavailable", available: false }],
15
+ },
16
+ provider: { name: "capability-e2e-provider", async decide() { providerCalls += 1; throw new Error("deterministic selection should not call Jev"); } },
17
+ });
18
+ assert.equal(providerCalls, 0);
19
+ assert.equal(decision.status, "selected");
20
+ assert.equal(decision.selected, "model:verified");
21
+ assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").source, "e2e-manifest");
22
+ assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").verified, true);
23
+ console.log(JSON.stringify({
24
+ ok: true,
25
+ status: decision.status,
26
+ selected: decision.selected,
27
+ provider: decision.receipt.provider,
28
+ deterministic: true,
29
+ jev_calls: providerCalls,
30
+ candidate_metadata: decision.candidates.find((candidate) => candidate.id === decision.selected),
31
+ }, null, 2));