jev-layer 0.1.0 → 0.3.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 (88) hide show
  1. package/CHANGELOG.md +73 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +125 -126
  5. package/README.ru.md +140 -117
  6. package/README.zh-CN.md +139 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +64 -56
  9. package/bin/jev.mjs +215 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +23 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +306 -0
  17. package/docs/COMPATIBILITY.md +24 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
  19. package/docs/PROVIDERS.md +109 -0
  20. package/docs/SCHEMA-VERSIONING.md +101 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +156 -51
  31. package/integrations/hermes/plugin.yaml +11 -5
  32. package/integrations/hermes/schemas.py +120 -12
  33. package/integrations/omp/extension.js +106 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +176 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +115 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +583 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/computer.mjs +196 -0
  55. package/src/config.mjs +86 -75
  56. package/src/context-filter.mjs +56 -56
  57. package/src/contract.mjs +74 -72
  58. package/src/discovery.mjs +65 -65
  59. package/src/hermes-adapter.mjs +134 -0
  60. package/src/images.mjs +98 -0
  61. package/src/mcp-server.mjs +313 -210
  62. package/src/model-route-metrics.mjs +89 -0
  63. package/src/model-routing.mjs +87 -0
  64. package/src/providers/demo.mjs +52 -52
  65. package/src/providers/index.mjs +12 -0
  66. package/src/providers/typesafe.mjs +206 -126
  67. package/src/receipts.mjs +228 -226
  68. package/src/registry.mjs +109 -109
  69. package/src/relevance-filter.mjs +99 -99
  70. package/src/route.mjs +228 -221
  71. package/src/shadow-compaction.mjs +170 -0
  72. package/src/supervision.mjs +268 -244
  73. package/test/browser.test.mjs +222 -199
  74. package/test/capability.test.mjs +54 -54
  75. package/test/computer.test.mjs +195 -0
  76. package/test/context-filter.test.mjs +65 -65
  77. package/test/hermes-adapter.test.mjs +63 -0
  78. package/test/image-fixture.mjs +2 -0
  79. package/test/images.test.mjs +85 -0
  80. package/test/model-route-metrics.test.mjs +61 -0
  81. package/test/model-routing.test.mjs +59 -0
  82. package/test/openrouter-provider.test.mjs +55 -55
  83. package/test/provider-transports.test.mjs +205 -0
  84. package/test/providers.test.mjs +125 -0
  85. package/test/receipts.test.mjs +71 -71
  86. package/test/route.test.mjs +86 -74
  87. package/test/shadow-compaction.test.mjs +67 -0
  88. package/test/supervision.test.mjs +117 -99
@@ -1,199 +1,222 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, readFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { test } from "node:test";
6
- import { buildBrowserRequest, browserActions, decideBrowserStep, runBrowserFastPath } from "../src/browser.mjs";
7
- import { readRoutingCases } from "../src/receipts.mjs";
8
- import { routeRequest } from "../src/route.mjs";
9
- function providerFor(choices) {
10
- let call = 0;
11
- return {
12
- name: "browser-test-provider",
13
- async decide({ candidates }) {
14
- const requested = choices[call++] ?? "browser:handoff";
15
- const choice = candidates.some((candidate) => candidate.id === requested)
16
- ? requested
17
- : candidates.find((candidate) => candidate.id === "browser:handoff")?.id ?? candidates[0]?.id;
18
- return {
19
- answers: { tool: { type: "choice", choice, probabilities: Object.fromEntries(candidates.map((candidate) => [candidate.id, candidate.id === choice ? 1 : 0])), confidence: 1 } },
20
- usage: { cost: 0.0001 },
21
- };
22
- },
23
- };
24
- }
25
-
26
- test("browser request exposes only bounded structured actions", () => {
27
- const request = buildBrowserRequest({
28
- goal: "inspect the visible page",
29
- observation: {
30
- url: "https://example.test",
31
- visible_text: "Visible content",
32
- scroll: { up: false, down: true },
33
- targets: [{ id: "submit", name: "Submit", role: "button", clickable: true }, { id: "country", name: "Country", options: [{ id: "de", label: "Germany" }] }],
34
- tabs: [{ id: "tab-1", title: "Current", active: true }, { id: "tab-2", title: "Other", active: false }],
35
- },
36
- });
37
- assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:scroll:down"));
38
- assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:click:submit"));
39
- assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:select:country:de"));
40
- assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:switch_tab:tab-2"));
41
- assert.ok(request.capabilities.every((candidate) => candidate.permissions.includes("browser")));
42
- assert.equal(request.capabilities.some((candidate) => candidate.id.includes("type")), false);
43
- });
44
-
45
- test("browser progress removes completed targets and exposes link navigation", () => {
46
- const request = buildBrowserRequest({
47
- goal: "click the action then navigate",
48
- observation: {
49
- url: "https://example.test/start",
50
- targets: [
51
- { id: "action", role: "button", name: "Action", clickable: true },
52
- { id: "next", role: "a", name: "Next", href: "/next", clickable: true },
53
- ],
54
- },
55
- progress: {
56
- executed_actions: [{ id: "browser:click:action", operation: "click", target_id: "action", status: "completed" }],
57
- used_targets: ["action"],
58
- current_url: "https://example.test/start",
59
- last_action: { id: "browser:click:action", operation: "click", target_id: "action", status: "completed" },
60
- last_result: { status: "completed", state_changed: true },
61
- },
62
- });
63
- assert.equal(request.capabilities.some((candidate) => candidate.id === "browser:click:action"), false);
64
- assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next"));
65
- assert.equal(request.context.browser.progress.goal, "click the action then navigate");
66
- assert.equal(request.context.browser.progress.current_url, "https://example.test/start");
67
- assert.equal(request.context.browser.progress.last_action.target_id, "action");
68
- });
69
-
70
- test("browser progress infers actionable roles and deterministic goal order", () => {
71
- const request = buildBrowserRequest({
72
- goal: "Click Increment, then select Beta, then navigate to the next page",
73
- observation: {
74
- url: "https://example.test/start",
75
- targets: [
76
- { id: "increment", role: "button", name: "Increment" },
77
- { id: "choice", role: "select", name: "Choice", options: [{ id: "beta", value: "beta", label: "Beta" }] },
78
- { id: "next", role: "a", name: "Next", href: "/next" },
79
- ],
80
- },
81
- });
82
- assert.deepEqual(request.context.browser.progress.remaining_expected_progress, [
83
- "browser:click:increment",
84
- "browser:select:choice:beta",
85
- "browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next",
86
- ]);
87
- assert.deepEqual(request.capabilities.map((candidate) => candidate.id), ["browser:click:increment", "browser:handoff"]);
88
- });
89
-
90
- test("browser progress only permits repeated scroll after measurable progress", () => {
91
- const base = {
92
- goal: "scroll down",
93
- observation: { url: "https://example.test", scroll: { down: true, position: 100, max: 1000 } },
94
- };
95
- const blocked = browserActions({
96
- ...base,
97
- progress: {
98
- last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
99
- last_result: { status: "completed", state_changed: false, scroll_progress: false },
100
- },
101
- });
102
- assert.equal(blocked.some((candidate) => candidate.id === "browser:scroll:down"), false);
103
- const allowed = browserActions({
104
- ...base,
105
- progress: {
106
- last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
107
- last_result: { status: "completed", state_changed: true, scroll_progress: true },
108
- },
109
- });
110
- assert.ok(allowed.some((candidate) => candidate.id === "browser:scroll:down"));
111
- });
112
-
113
- test("browser fast-path is opt-in and fails open without an executor", async () => {
114
- const disabled = await decideBrowserStep({ goal: "inspect", observation: {} }, { enabled: false, provider: "demo" });
115
- assert.equal(disabled.decision.status, "fallback");
116
- assert.equal(disabled.decision.fallback.type, "browser_fast_path_disabled");
117
- const unavailable = await runBrowserFastPath({ goal: "inspect", enabled: true, provider: "demo" });
118
- assert.equal(unavailable.status, "handoff");
119
- assert.equal(unavailable.reason, "browser_executor_unavailable");
120
- });
121
-
122
- test("browser fast-path executes safe actions then hands control back", async () => {
123
- const path = join(await mkdtemp(join(tmpdir(), "jev-browser-")), "cases.jsonl");
124
- const executed = [];
125
- const observations = [
126
- { url: "https://example.test", scroll: { up: false, down: true }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
127
- { url: "https://example.test", scroll: { up: false, down: false }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
128
- ];
129
- const executor = {
130
- async observe() { return observations.shift() ?? observations.at(-1) ?? {}; },
131
- async execute(action) {
132
- executed.push(action);
133
- return { result: { operation: action.operation }, observation: observations.shift() ?? {}, handoff: executed.length === 2 };
134
- },
135
- };
136
- const result = await runBrowserFastPath({
137
- goal: "scroll and switch to the other tab",
138
- harness: "browser-test",
139
- enabled: true,
140
- provider: providerFor(["browser:scroll:down", "browser:switch_tab:tab-2"]),
141
- executor,
142
- receiptPath: path,
143
- });
144
- assert.equal(result.status, "handoff");
145
- assert.equal(result.reason, "host_handoff");
146
- assert.deepEqual(executed.map((action) => action.operation), ["scroll", "switch_tab"]);
147
- assert.equal(result.metrics.jev_calls, 2);
148
- assert.equal(result.metrics.browser_actions, 2);
149
- assert.equal(result.metrics.failures, 0);
150
- assert.equal(result.metrics.cost_usd, 0.0002);
151
- const records = (await readFile(path, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
152
- assert.equal(records.length, 4);
153
- assert.equal(records.filter((record) => record.record_type === "routing_case").length, 2);
154
- assert.equal(records.filter((record) => record.record_type === "execution_receipt").length, 2);
155
- assert.equal(records[1].host.browser.operation, "scroll");
156
- assert.equal(records[3].host.browser.operation, "switch_tab");
157
- const replayCases = await readRoutingCases(path);
158
- const replayed = await Promise.all(replayCases.map((routingCase) => routeRequest(routingCase.request, { provider: "demo" })));
159
- assert.equal(replayed.length, 2);
160
- assert.ok(replayed.every((decision) => decision.execution.enabled === false));
161
- });
162
-
163
- test("consequential browser action hands off for host approval", async () => {
164
- let executeCalls = 0;
165
- const result = await runBrowserFastPath({
166
- goal: "click the visible action",
167
- enabled: true,
168
- provider: providerFor(["browser:click:action"]),
169
- executor: {
170
- async observe() { return { targets: [{ id: "action", name: "Action", clickable: true }] }; },
171
- async approve() { return false; },
172
- async execute() { executeCalls += 1; return {}; },
173
- },
174
- });
175
- assert.equal(result.status, "handoff");
176
- assert.equal(result.reason, "host_confirmation_required");
177
- assert.equal(executeCalls, 0);
178
- });
179
-
180
- test("native select uses the optional host select executor", async () => {
181
- let selectCalls = 0;
182
- let executeCalls = 0;
183
- const result = await runBrowserFastPath({
184
- goal: "select beta",
185
- enabled: true,
186
- maxSteps: 1,
187
- provider: providerFor(["browser:select:choice:beta"]),
188
- executor: {
189
- async observe() { return { targets: [{ id: "choice", role: "select", options: [{ id: "beta", label: "Beta" }] }] }; },
190
- async approve() { return true; },
191
- async select(action) { selectCalls += 1; return { result: { value: action.option_id }, observation: { targets: [] } }; },
192
- async execute() { executeCalls += 1; },
193
- },
194
- });
195
- assert.equal(result.reason, "browser_step_budget_exhausted");
196
- assert.equal(result.metrics.browser_actions, 1);
197
- assert.equal(selectCalls, 1);
198
- assert.equal(executeCalls, 0);
199
- });
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { buildBrowserRequest, browserActions, decideBrowserStep, runBrowserFastPath } from "../src/browser.mjs";
7
+ import { readRoutingCases } from "../src/receipts.mjs";
8
+ import { routeRequest } from "../src/route.mjs";
9
+ import { buildExecutionReceipt, buildRoutingCase } from "../src/receipts.mjs";
10
+ function providerFor(choices) {
11
+ let call = 0;
12
+ return {
13
+ name: "browser-test-provider",
14
+ async decide({ candidates }) {
15
+ const requested = choices[call++] ?? "browser:handoff";
16
+ const choice = candidates.some((candidate) => candidate.id === requested)
17
+ ? requested
18
+ : candidates.find((candidate) => candidate.id === "browser:handoff")?.id ?? candidates[0]?.id;
19
+ return {
20
+ answers: { tool: { type: "choice", choice, probabilities: Object.fromEntries(candidates.map((candidate) => [candidate.id, candidate.id === choice ? 1 : 0])), confidence: 1 } },
21
+ usage: { cost: 0.0001 },
22
+ };
23
+ },
24
+ };
25
+ }
26
+
27
+ test("browser request exposes only bounded structured actions", () => {
28
+ const request = buildBrowserRequest({
29
+ goal: "inspect the visible page",
30
+ observation: {
31
+ url: "https://example.test",
32
+ visible_text: "Visible content",
33
+ scroll: { up: false, down: true },
34
+ targets: [{ id: "submit", name: "Submit", role: "button", clickable: true }, { id: "country", name: "Country", options: [{ id: "de", label: "Germany" }] }],
35
+ tabs: [{ id: "tab-1", title: "Current", active: true }, { id: "tab-2", title: "Other", active: false }],
36
+ },
37
+ });
38
+ assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:scroll:down"));
39
+ assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:click:submit"));
40
+ assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:select:country:de"));
41
+ assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:switch_tab:tab-2"));
42
+ assert.ok(request.capabilities.every((candidate) => candidate.permissions.includes("browser")));
43
+ assert.equal(request.capabilities.some((candidate) => candidate.id.includes("type")), false);
44
+ });
45
+
46
+ test("browser progress removes completed targets and exposes link navigation", () => {
47
+ const request = buildBrowserRequest({
48
+ goal: "click the action then navigate",
49
+ observation: {
50
+ url: "https://example.test/start",
51
+ targets: [
52
+ { id: "action", role: "button", name: "Action", clickable: true },
53
+ { id: "next", role: "a", name: "Next", href: "/next", clickable: true },
54
+ ],
55
+ },
56
+ progress: {
57
+ executed_actions: [{ id: "browser:click:action", operation: "click", target_id: "action", status: "completed" }],
58
+ used_targets: ["action"],
59
+ current_url: "https://example.test/start",
60
+ last_action: { id: "browser:click:action", operation: "click", target_id: "action", status: "completed" },
61
+ last_result: { status: "completed", state_changed: true },
62
+ },
63
+ });
64
+ assert.equal(request.capabilities.some((candidate) => candidate.id === "browser:click:action"), false);
65
+ assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next"));
66
+ assert.equal(request.context.browser.progress.goal, "click the action then navigate");
67
+ assert.equal(request.context.browser.progress.current_url, "https://example.test/start");
68
+ assert.equal(request.context.browser.progress.last_action.target_id, "action");
69
+ });
70
+
71
+ test("browser progress infers actionable roles and deterministic goal order", () => {
72
+ const request = buildBrowserRequest({
73
+ goal: "Click Increment, then select Beta, then navigate to the next page",
74
+ observation: {
75
+ url: "https://example.test/start",
76
+ targets: [
77
+ { id: "increment", role: "button", name: "Increment" },
78
+ { id: "choice", role: "select", name: "Choice", options: [{ id: "beta", value: "beta", label: "Beta" }] },
79
+ { id: "next", role: "a", name: "Next", href: "/next" },
80
+ ],
81
+ },
82
+ });
83
+ assert.deepEqual(request.context.browser.progress.remaining_expected_progress, [
84
+ "browser:click:increment",
85
+ "browser:select:choice:beta",
86
+ "browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next",
87
+ ]);
88
+ assert.deepEqual(request.capabilities.map((candidate) => candidate.id), ["browser:click:increment", "browser:handoff"]);
89
+ });
90
+
91
+ test("browser progress only permits repeated scroll after measurable progress", () => {
92
+ const base = {
93
+ goal: "scroll down",
94
+ observation: { url: "https://example.test", scroll: { down: true, position: 100, max: 1000 } },
95
+ };
96
+ const blocked = browserActions({
97
+ ...base,
98
+ progress: {
99
+ last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
100
+ last_result: { status: "completed", state_changed: false, scroll_progress: false },
101
+ },
102
+ });
103
+ assert.equal(blocked.some((candidate) => candidate.id === "browser:scroll:down"), false);
104
+ const allowed = browserActions({
105
+ ...base,
106
+ progress: {
107
+ last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
108
+ last_result: { status: "completed", state_changed: true, scroll_progress: true },
109
+ },
110
+ });
111
+ assert.ok(allowed.some((candidate) => candidate.id === "browser:scroll:down"));
112
+ });
113
+
114
+ test("browser fast-path is opt-in and fails open without an executor", async () => {
115
+ const disabled = await decideBrowserStep({ goal: "inspect", observation: {} }, { enabled: false, provider: "demo" });
116
+ assert.equal(disabled.decision.status, "fallback");
117
+ assert.equal(disabled.decision.fallback.type, "browser_fast_path_disabled");
118
+ const unavailable = await runBrowserFastPath({ goal: "inspect", enabled: true, provider: "demo" });
119
+ assert.equal(unavailable.status, "handoff");
120
+ assert.equal(unavailable.reason, "browser_executor_unavailable");
121
+ });
122
+
123
+ test("browser fast-path executes safe actions then hands control back", async () => {
124
+ const path = join(await mkdtemp(join(tmpdir(), "jev-browser-")), "cases.jsonl");
125
+ const executed = [];
126
+ const observations = [
127
+ { url: "https://example.test", scroll: { up: false, down: true }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
128
+ { url: "https://example.test", scroll: { up: false, down: false }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
129
+ ];
130
+ const executor = {
131
+ async observe() { return observations.shift() ?? observations.at(-1) ?? {}; },
132
+ async execute(action) {
133
+ executed.push(action);
134
+ return { result: { operation: action.operation }, observation: observations.shift() ?? {}, handoff: executed.length === 2 };
135
+ },
136
+ };
137
+ const result = await runBrowserFastPath({
138
+ goal: "scroll and switch to the other tab",
139
+ harness: "browser-test",
140
+ enabled: true,
141
+ provider: providerFor(["browser:scroll:down", "browser:switch_tab:tab-2"]),
142
+ executor,
143
+ receiptPath: path,
144
+ });
145
+ assert.equal(result.status, "handoff");
146
+ assert.equal(result.reason, "host_handoff");
147
+ assert.deepEqual(executed.map((action) => action.operation), ["scroll", "switch_tab"]);
148
+ assert.equal(result.metrics.jev_calls, 2);
149
+ assert.equal(result.metrics.browser_actions, 2);
150
+ assert.equal(result.metrics.failures, 0);
151
+ assert.equal(result.metrics.cost_usd, 0.0002);
152
+ const records = (await readFile(path, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
153
+ assert.equal(records.length, 4);
154
+ assert.equal(records.filter((record) => record.record_type === "routing_case").length, 2);
155
+ assert.equal(records.filter((record) => record.record_type === "execution_receipt").length, 2);
156
+ assert.equal(records[1].host.browser.operation, "scroll");
157
+ assert.equal(records[3].host.browser.operation, "switch_tab");
158
+ const replayCases = await readRoutingCases(path);
159
+ const replayed = await Promise.all(replayCases.map((routingCase) => routeRequest(routingCase.request, { provider: "demo" })));
160
+ assert.equal(replayed.length, 2);
161
+ assert.ok(replayed.every((decision) => decision.execution.enabled === false));
162
+ });
163
+
164
+ test("consequential browser action hands off for host approval", async () => {
165
+ let executeCalls = 0;
166
+ const result = await runBrowserFastPath({
167
+ goal: "click the visible action",
168
+ enabled: true,
169
+ provider: providerFor(["browser:click:action"]),
170
+ executor: {
171
+ async observe() { return { targets: [{ id: "action", name: "Action", clickable: true }] }; },
172
+ async approve() { return false; },
173
+ async execute() { executeCalls += 1; return {}; },
174
+ },
175
+ });
176
+ assert.equal(result.status, "handoff");
177
+ assert.equal(result.reason, "host_confirmation_required");
178
+ assert.equal(executeCalls, 0);
179
+ });
180
+
181
+ test("native select uses the optional host select executor", async () => {
182
+ let selectCalls = 0;
183
+ let executeCalls = 0;
184
+ const result = await runBrowserFastPath({
185
+ goal: "select beta",
186
+ enabled: true,
187
+ maxSteps: 1,
188
+ provider: providerFor(["browser:select:choice:beta"]),
189
+ executor: {
190
+ async observe() { return { targets: [{ id: "choice", role: "select", options: [{ id: "beta", label: "Beta" }] }] }; },
191
+ async approve() { return true; },
192
+ async select(action) { selectCalls += 1; return { result: { value: action.option_id }, observation: { targets: [] } }; },
193
+ async execute() { executeCalls += 1; },
194
+ },
195
+ });
196
+ assert.equal(result.reason, "browser_step_budget_exhausted");
197
+ assert.equal(result.metrics.browser_actions, 1);
198
+ assert.equal(selectCalls, 1);
199
+ assert.equal(executeCalls, 0);
200
+ });
201
+
202
+ test("screenshots stay out of browser text context and durable replay", async () => {
203
+ const imageData = "private-screen-pixels";
204
+ const request = buildBrowserRequest({
205
+ goal: "inspect the screen",
206
+ observation: {
207
+ screenshot: { format: "png", data_base64: imageData, width: 280, height: 280 },
208
+ targets: [{ id: "inspect", role: "button", name: "Inspect" }],
209
+ },
210
+ });
211
+ assert.equal(JSON.stringify(request.context).includes(imageData), false);
212
+ const decision = await routeRequest(request, { provider: "demo" });
213
+ assert.equal(decision.status, "fallback");
214
+ const recorded = buildRoutingCase({ request, decision });
215
+ assert.equal(JSON.stringify(recorded).includes(imageData), false);
216
+ assert.equal(recorded.request.assets_omitted, true);
217
+ assert.equal(typeof recorded.request.context.browser.observation.screenshot.sha256, "string");
218
+ const execution = buildExecutionReceipt({ request, decision, host: {
219
+ status: "completed", result: { observation: { screenshot: { format: "png", data_base64: imageData } } },
220
+ } });
221
+ assert.equal(JSON.stringify(execution).includes(imageData), false);
222
+ });
@@ -1,54 +1,54 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
- import { discoverCapabilities } from "../src/discovery.mjs";
4
- import { routeRequest } from "../src/route.mjs";
5
-
6
- function discovered() {
7
- return discoverCapabilities({
8
- skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect work", verified: true }],
9
- mcp: [{ id: "mcp:search", name: "Search MCP", description: "Search files" }],
10
- cli: [{ id: "cli:git", name: "Git CLI", command: "git", description: "Run git" }],
11
- dsh: [{ id: "dsh:worker", name: "Worker DSH", description: "Delegate work" }],
12
- subagents: [{ id: "subagent:review", name: "Review subagent", description: "Review code" }],
13
- models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions" }],
14
- tools: [{ id: "tool:read", name: "Read tool", description: "Read files" }],
15
- });
16
- }
17
-
18
- test("discovery normalizes skills, MCP, CLI, DSH, subagent, model, and tool candidates", () => {
19
- const capabilities = discovered();
20
- assert.deepEqual(new Set(capabilities.map((capability) => capability.kind)), new Set(["skill", "mcp", "cli", "dsh", "subagent", "model", "tool"]));
21
- assert.equal(capabilities.find((capability) => capability.id === "cli:git").metadata.cli, "git");
22
- assert.equal(capabilities.find((capability) => capability.id === "skill:inspect").verified, true);
23
- });
24
-
25
- test("explicit deterministic policy selects one discovered capability before Jev", async () => {
26
- let providerCalls = 0;
27
- const decision = await routeRequest({
28
- intent: "make a bounded model decision",
29
- context: { request: "model" },
30
- policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
31
- }, {
32
- discovery: { models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions", verified: true, risk: "low" }] },
33
- provider: { name: "must-not-run", async decide() { providerCalls += 1; throw new Error("deterministic route should not call Jev"); } },
34
- });
35
- assert.equal(providerCalls, 0);
36
- assert.equal(decision.status, "selected");
37
- assert.equal(decision.selected, "model:jev");
38
- assert.equal(decision.receipt.provider, "must-not-run:deterministic");
39
- assert.equal(decision.candidates[0].verified, true);
40
- });
41
-
42
- test("availability policy excludes an unavailable discovered capability", async () => {
43
- const decision = await routeRequest({
44
- intent: "inspect",
45
- context: {},
46
- policy: { deterministic: true, deterministic_capability_id: "tool:offline" },
47
- }, {
48
- discovery: { tools: [{ id: "tool:offline", name: "Offline", description: "Unavailable", available: false }] },
49
- provider: "demo",
50
- });
51
- assert.equal(decision.status, "no_decision");
52
- assert.equal(decision.candidates[0].filtered, true);
53
- assert.equal(decision.candidates[0].filter_reason, "capability unavailable");
54
- });
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { discoverCapabilities } from "../src/discovery.mjs";
4
+ import { routeRequest } from "../src/route.mjs";
5
+
6
+ function discovered() {
7
+ return discoverCapabilities({
8
+ skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect work", verified: true }],
9
+ mcp: [{ id: "mcp:search", name: "Search MCP", description: "Search files" }],
10
+ cli: [{ id: "cli:git", name: "Git CLI", command: "git", description: "Run git" }],
11
+ dsh: [{ id: "dsh:worker", name: "Worker DSH", description: "Delegate work" }],
12
+ subagents: [{ id: "subagent:review", name: "Review subagent", description: "Review code" }],
13
+ models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions" }],
14
+ tools: [{ id: "tool:read", name: "Read tool", description: "Read files" }],
15
+ });
16
+ }
17
+
18
+ test("discovery normalizes skills, MCP, CLI, DSH, subagent, model, and tool candidates", () => {
19
+ const capabilities = discovered();
20
+ assert.deepEqual(new Set(capabilities.map((capability) => capability.kind)), new Set(["skill", "mcp", "cli", "dsh", "subagent", "model", "tool"]));
21
+ assert.equal(capabilities.find((capability) => capability.id === "cli:git").metadata.cli, "git");
22
+ assert.equal(capabilities.find((capability) => capability.id === "skill:inspect").verified, true);
23
+ });
24
+
25
+ test("explicit deterministic policy selects one discovered capability before Jev", async () => {
26
+ let providerCalls = 0;
27
+ const decision = await routeRequest({
28
+ intent: "make a bounded model decision",
29
+ context: { request: "model" },
30
+ policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
31
+ }, {
32
+ discovery: { models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions", verified: true, risk: "low" }] },
33
+ provider: { name: "must-not-run", async decide() { providerCalls += 1; throw new Error("deterministic route should not call Jev"); } },
34
+ });
35
+ assert.equal(providerCalls, 0);
36
+ assert.equal(decision.status, "selected");
37
+ assert.equal(decision.selected, "model:jev");
38
+ assert.equal(decision.receipt.provider, "must-not-run:deterministic");
39
+ assert.equal(decision.candidates[0].verified, true);
40
+ });
41
+
42
+ test("availability policy excludes an unavailable discovered capability", async () => {
43
+ const decision = await routeRequest({
44
+ intent: "inspect",
45
+ context: {},
46
+ policy: { deterministic: true, deterministic_capability_id: "tool:offline" },
47
+ }, {
48
+ discovery: { tools: [{ id: "tool:offline", name: "Offline", description: "Unavailable", available: false }] },
49
+ provider: "demo",
50
+ });
51
+ assert.equal(decision.status, "no_decision");
52
+ assert.equal(decision.candidates[0].filtered, true);
53
+ assert.equal(decision.candidates[0].filter_reason, "capability unavailable");
54
+ });