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.
- package/CHANGELOG.md +73 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +125 -126
- package/README.ru.md +140 -117
- package/README.zh-CN.md +139 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +64 -56
- package/bin/jev.mjs +215 -213
- package/config/codex.mcp.toml +8 -8
- package/config/generic-mcp.json +13 -13
- package/config/hermes.mcp.yaml +9 -9
- package/config/jev.example.json +23 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +306 -0
- package/docs/COMPATIBILITY.md +24 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
- package/docs/PROVIDERS.md +109 -0
- package/docs/SCHEMA-VERSIONING.md +101 -94
- package/docs/architecture.svg +77 -0
- package/docs/jev-layer-banner.svg +38 -0
- package/examples/capabilities.json +39 -39
- package/examples/route-request.json +28 -28
- package/integrations/codex/.codex-plugin/plugin.json +19 -19
- package/integrations/codex/.mcp.json +11 -11
- package/integrations/codex/AGENTS.md +1 -1
- package/integrations/codex/run-mcp.mjs +9 -9
- package/integrations/codex/skills/jev-route/SKILL.md +17 -17
- package/integrations/hermes/__init__.py +156 -51
- package/integrations/hermes/plugin.yaml +11 -5
- package/integrations/hermes/schemas.py +120 -12
- package/integrations/omp/extension.js +106 -105
- package/integrations/template/README.md +10 -10
- package/integrations/template/adapter.mjs +87 -87
- package/package.json +60 -59
- package/scripts/benchmark.mjs +35 -35
- package/scripts/browser-benchmark.mjs +92 -92
- package/scripts/browser-e2e.mjs +117 -117
- package/scripts/capability-e2e.mjs +31 -31
- package/scripts/clean-install-smoke.mjs +176 -162
- package/scripts/codex-mcp-smoke.mjs +119 -119
- package/scripts/context-filter-e2e.mjs +45 -45
- package/scripts/fail-open-smoke.mjs +120 -120
- package/scripts/feature-flags-smoke.mjs +46 -46
- package/scripts/mcp-receipt-smoke.mjs +115 -98
- package/scripts/model-route-report.mjs +15 -0
- package/scripts/openrouter-choice.mjs +59 -59
- package/scripts/replay-eval.mjs +124 -124
- package/scripts/smoke.mjs +34 -34
- package/scripts/supervision-e2e.mjs +138 -109
- package/src/browser.mjs +583 -569
- package/src/cli.mjs +33 -33
- package/src/computer.mjs +196 -0
- package/src/config.mjs +86 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +74 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +134 -0
- package/src/images.mjs +98 -0
- package/src/mcp-server.mjs +313 -210
- package/src/model-route-metrics.mjs +89 -0
- package/src/model-routing.mjs +87 -0
- package/src/providers/demo.mjs +52 -52
- package/src/providers/index.mjs +12 -0
- package/src/providers/typesafe.mjs +206 -126
- package/src/receipts.mjs +228 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +228 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +222 -199
- package/test/capability.test.mjs +54 -54
- package/test/computer.test.mjs +195 -0
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +63 -0
- package/test/image-fixture.mjs +2 -0
- package/test/images.test.mjs +85 -0
- package/test/model-route-metrics.test.mjs +61 -0
- package/test/model-routing.test.mjs +59 -0
- package/test/openrouter-provider.test.mjs +55 -55
- package/test/provider-transports.test.mjs +205 -0
- package/test/providers.test.mjs +125 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +86 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { routeRequest } from "../src/route.mjs";
|
|
3
|
-
|
|
4
|
-
const messages = [
|
|
5
|
-
...Array.from({ length: 12 }, (_, index) => `stale unrelated worker note ${index}`),
|
|
6
|
-
"Requirement: preserve /workspace/project/config.json exactly",
|
|
7
|
-
"Error: command `npm test` failed with exit status 1",
|
|
8
|
-
"recent worker state",
|
|
9
|
-
];
|
|
10
|
-
const context = { messages };
|
|
11
|
-
const before = JSON.stringify(context);
|
|
12
|
-
let projected;
|
|
13
|
-
const decision = await routeRequest({
|
|
14
|
-
harness: "context-filter-e2e",
|
|
15
|
-
intent: "inspect project state",
|
|
16
|
-
context,
|
|
17
|
-
policy: { context_filter_mode: "conservative" },
|
|
18
|
-
capabilities: [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect context", risk: "low" }],
|
|
19
|
-
}, {
|
|
20
|
-
provider: {
|
|
21
|
-
name: "context-filter-e2e-provider",
|
|
22
|
-
async decide({ state }) {
|
|
23
|
-
projected = state;
|
|
24
|
-
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
assert.equal(decision.status, "selected");
|
|
29
|
-
assert.equal(projected.context_filter.mode, "conservative");
|
|
30
|
-
assert.ok(projected.context_filter.dropped > 0);
|
|
31
|
-
assert.ok(projected.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
32
|
-
assert.ok(projected.context.messages.includes("Error: command `npm test` failed with exit status 1"));
|
|
33
|
-
assert.equal(JSON.stringify(context), before);
|
|
34
|
-
console.log(JSON.stringify({
|
|
35
|
-
ok: true,
|
|
36
|
-
status: decision.status,
|
|
37
|
-
mode: projected.context_filter.mode,
|
|
38
|
-
kept: projected.context_filter.kept,
|
|
39
|
-
dropped: projected.context_filter.dropped,
|
|
40
|
-
pinned: projected.context_filter.pinned,
|
|
41
|
-
preserved: [
|
|
42
|
-
projected.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"),
|
|
43
|
-
projected.context.messages.includes("Error: command `npm test` failed with exit status 1"),
|
|
44
|
-
],
|
|
45
|
-
}, null, 2));
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { routeRequest } from "../src/route.mjs";
|
|
3
|
+
|
|
4
|
+
const messages = [
|
|
5
|
+
...Array.from({ length: 12 }, (_, index) => `stale unrelated worker note ${index}`),
|
|
6
|
+
"Requirement: preserve /workspace/project/config.json exactly",
|
|
7
|
+
"Error: command `npm test` failed with exit status 1",
|
|
8
|
+
"recent worker state",
|
|
9
|
+
];
|
|
10
|
+
const context = { messages };
|
|
11
|
+
const before = JSON.stringify(context);
|
|
12
|
+
let projected;
|
|
13
|
+
const decision = await routeRequest({
|
|
14
|
+
harness: "context-filter-e2e",
|
|
15
|
+
intent: "inspect project state",
|
|
16
|
+
context,
|
|
17
|
+
policy: { context_filter_mode: "conservative" },
|
|
18
|
+
capabilities: [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect context", risk: "low" }],
|
|
19
|
+
}, {
|
|
20
|
+
provider: {
|
|
21
|
+
name: "context-filter-e2e-provider",
|
|
22
|
+
async decide({ state }) {
|
|
23
|
+
projected = state;
|
|
24
|
+
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
assert.equal(decision.status, "selected");
|
|
29
|
+
assert.equal(projected.context_filter.mode, "conservative");
|
|
30
|
+
assert.ok(projected.context_filter.dropped > 0);
|
|
31
|
+
assert.ok(projected.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
32
|
+
assert.ok(projected.context.messages.includes("Error: command `npm test` failed with exit status 1"));
|
|
33
|
+
assert.equal(JSON.stringify(context), before);
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
ok: true,
|
|
36
|
+
status: decision.status,
|
|
37
|
+
mode: projected.context_filter.mode,
|
|
38
|
+
kept: projected.context_filter.kept,
|
|
39
|
+
dropped: projected.context_filter.dropped,
|
|
40
|
+
pinned: projected.context_filter.pinned,
|
|
41
|
+
preserved: [
|
|
42
|
+
projected.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"),
|
|
43
|
+
projected.context.messages.includes("Error: command `npm test` failed with exit status 1"),
|
|
44
|
+
],
|
|
45
|
+
}, null, 2));
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { routeRequest } from "../src/route.mjs";
|
|
3
|
-
|
|
4
|
-
const candidates = [
|
|
5
|
-
{
|
|
6
|
-
id: "read_manifest",
|
|
7
|
-
name: "read",
|
|
8
|
-
kind: "tool",
|
|
9
|
-
description: "Read the repository package manifest without modifying it.",
|
|
10
|
-
execution: { mode: "native", target: "read" },
|
|
11
|
-
permissions: ["read"],
|
|
12
|
-
risk: "low",
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: "read_route",
|
|
16
|
-
name: "read",
|
|
17
|
-
kind: "tool",
|
|
18
|
-
description: "Read the routing implementation without modifying it.",
|
|
19
|
-
execution: { mode: "native", target: "read" },
|
|
20
|
-
permissions: ["read"],
|
|
21
|
-
risk: "low",
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const request = {
|
|
26
|
-
intent: "inspect repository metadata",
|
|
27
|
-
harness: "fail-open-smoke",
|
|
28
|
-
context: { workspace: "jev-layer", operation: "read-only" },
|
|
29
|
-
actor_permissions: ["read"],
|
|
30
|
-
capabilities: candidates,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const originalEnabled = process.env.JEV_LAYER_ENABLED;
|
|
34
|
-
const disabled = await withEnv("JEV_LAYER_ENABLED", "0", () => routeRequest(request, { provider: "openrouter" }));
|
|
35
|
-
restoreEnv("JEV_LAYER_ENABLED", originalEnabled);
|
|
36
|
-
assert.equal(disabled.status, "fallback");
|
|
37
|
-
assert.equal(disabled.fallback.type, "disabled");
|
|
38
|
-
assert.equal(disabled.execution.enabled, false);
|
|
39
|
-
|
|
40
|
-
const unavailable = await routeRequest(request, {
|
|
41
|
-
provider: "openrouter",
|
|
42
|
-
openrouter: {
|
|
43
|
-
apiKey: "smoke-token",
|
|
44
|
-
endpoint: "http://127.0.0.1:1/unavailable",
|
|
45
|
-
timeoutMs: 100,
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
assert.equal(unavailable.status, "fallback");
|
|
49
|
-
assert.equal(unavailable.fallback.type, "provider_error");
|
|
50
|
-
assert.equal(unavailable.execution.enabled, false);
|
|
51
|
-
|
|
52
|
-
const noDecision = await routeRequest({
|
|
53
|
-
...request,
|
|
54
|
-
capabilities: candidates.map((candidate) => ({
|
|
55
|
-
...candidate,
|
|
56
|
-
availability: { available: false, reason: "host capability intentionally unavailable" },
|
|
57
|
-
})),
|
|
58
|
-
}, { provider: "demo" });
|
|
59
|
-
assert.equal(noDecision.status, "no_decision");
|
|
60
|
-
assert.equal(noDecision.execution.enabled, false);
|
|
61
|
-
|
|
62
|
-
const lowConfidence = await routeRequest(request, {
|
|
63
|
-
provider: {
|
|
64
|
-
name: "low-confidence-smoke-provider",
|
|
65
|
-
async decide() {
|
|
66
|
-
return {
|
|
67
|
-
answers: {
|
|
68
|
-
tool: {
|
|
69
|
-
type: "choice",
|
|
70
|
-
choice: "read_manifest",
|
|
71
|
-
probabilities: { read_manifest: 0.51, read_route: 0.49 },
|
|
72
|
-
confidence: 0.51,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
policy: { min_confidence: 0.8 },
|
|
79
|
-
});
|
|
80
|
-
assert.equal(lowConfidence.status, "fallback");
|
|
81
|
-
assert.equal(lowConfidence.fallback.type, "low_confidence");
|
|
82
|
-
assert.equal(lowConfidence.selected, "read_manifest");
|
|
83
|
-
assert.equal(lowConfidence.execution.enabled, false);
|
|
84
|
-
|
|
85
|
-
console.log(JSON.stringify({
|
|
86
|
-
ok: true,
|
|
87
|
-
scenarios: [
|
|
88
|
-
summarize("disabled", disabled),
|
|
89
|
-
summarize("provider_unavailable", unavailable),
|
|
90
|
-
summarize("no_decision", noDecision),
|
|
91
|
-
summarize("low_confidence", lowConfidence),
|
|
92
|
-
],
|
|
93
|
-
}, null, 2));
|
|
94
|
-
|
|
95
|
-
async function withEnv(name, value, operation) {
|
|
96
|
-
const previous = process.env[name];
|
|
97
|
-
process.env[name] = value;
|
|
98
|
-
try {
|
|
99
|
-
return await operation();
|
|
100
|
-
} finally {
|
|
101
|
-
restoreEnv(name, previous);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function restoreEnv(name, value) {
|
|
106
|
-
if (value === undefined) delete process.env[name];
|
|
107
|
-
else process.env[name] = value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function summarize(name, result) {
|
|
111
|
-
return {
|
|
112
|
-
name,
|
|
113
|
-
status: result.status,
|
|
114
|
-
selected: result.selected,
|
|
115
|
-
fallback: result.fallback?.type ?? null,
|
|
116
|
-
confidence: result.confidence ?? null,
|
|
117
|
-
latency_ms: result.latency_ms,
|
|
118
|
-
execution: result.execution,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { routeRequest } from "../src/route.mjs";
|
|
3
|
+
|
|
4
|
+
const candidates = [
|
|
5
|
+
{
|
|
6
|
+
id: "read_manifest",
|
|
7
|
+
name: "read",
|
|
8
|
+
kind: "tool",
|
|
9
|
+
description: "Read the repository package manifest without modifying it.",
|
|
10
|
+
execution: { mode: "native", target: "read" },
|
|
11
|
+
permissions: ["read"],
|
|
12
|
+
risk: "low",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "read_route",
|
|
16
|
+
name: "read",
|
|
17
|
+
kind: "tool",
|
|
18
|
+
description: "Read the routing implementation without modifying it.",
|
|
19
|
+
execution: { mode: "native", target: "read" },
|
|
20
|
+
permissions: ["read"],
|
|
21
|
+
risk: "low",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const request = {
|
|
26
|
+
intent: "inspect repository metadata",
|
|
27
|
+
harness: "fail-open-smoke",
|
|
28
|
+
context: { workspace: "jev-layer", operation: "read-only" },
|
|
29
|
+
actor_permissions: ["read"],
|
|
30
|
+
capabilities: candidates,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const originalEnabled = process.env.JEV_LAYER_ENABLED;
|
|
34
|
+
const disabled = await withEnv("JEV_LAYER_ENABLED", "0", () => routeRequest(request, { provider: "openrouter" }));
|
|
35
|
+
restoreEnv("JEV_LAYER_ENABLED", originalEnabled);
|
|
36
|
+
assert.equal(disabled.status, "fallback");
|
|
37
|
+
assert.equal(disabled.fallback.type, "disabled");
|
|
38
|
+
assert.equal(disabled.execution.enabled, false);
|
|
39
|
+
|
|
40
|
+
const unavailable = await routeRequest(request, {
|
|
41
|
+
provider: "openrouter",
|
|
42
|
+
openrouter: {
|
|
43
|
+
apiKey: "smoke-token",
|
|
44
|
+
endpoint: "http://127.0.0.1:1/unavailable",
|
|
45
|
+
timeoutMs: 100,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
assert.equal(unavailable.status, "fallback");
|
|
49
|
+
assert.equal(unavailable.fallback.type, "provider_error");
|
|
50
|
+
assert.equal(unavailable.execution.enabled, false);
|
|
51
|
+
|
|
52
|
+
const noDecision = await routeRequest({
|
|
53
|
+
...request,
|
|
54
|
+
capabilities: candidates.map((candidate) => ({
|
|
55
|
+
...candidate,
|
|
56
|
+
availability: { available: false, reason: "host capability intentionally unavailable" },
|
|
57
|
+
})),
|
|
58
|
+
}, { provider: "demo" });
|
|
59
|
+
assert.equal(noDecision.status, "no_decision");
|
|
60
|
+
assert.equal(noDecision.execution.enabled, false);
|
|
61
|
+
|
|
62
|
+
const lowConfidence = await routeRequest(request, {
|
|
63
|
+
provider: {
|
|
64
|
+
name: "low-confidence-smoke-provider",
|
|
65
|
+
async decide() {
|
|
66
|
+
return {
|
|
67
|
+
answers: {
|
|
68
|
+
tool: {
|
|
69
|
+
type: "choice",
|
|
70
|
+
choice: "read_manifest",
|
|
71
|
+
probabilities: { read_manifest: 0.51, read_route: 0.49 },
|
|
72
|
+
confidence: 0.51,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
policy: { min_confidence: 0.8 },
|
|
79
|
+
});
|
|
80
|
+
assert.equal(lowConfidence.status, "fallback");
|
|
81
|
+
assert.equal(lowConfidence.fallback.type, "low_confidence");
|
|
82
|
+
assert.equal(lowConfidence.selected, "read_manifest");
|
|
83
|
+
assert.equal(lowConfidence.execution.enabled, false);
|
|
84
|
+
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
ok: true,
|
|
87
|
+
scenarios: [
|
|
88
|
+
summarize("disabled", disabled),
|
|
89
|
+
summarize("provider_unavailable", unavailable),
|
|
90
|
+
summarize("no_decision", noDecision),
|
|
91
|
+
summarize("low_confidence", lowConfidence),
|
|
92
|
+
],
|
|
93
|
+
}, null, 2));
|
|
94
|
+
|
|
95
|
+
async function withEnv(name, value, operation) {
|
|
96
|
+
const previous = process.env[name];
|
|
97
|
+
process.env[name] = value;
|
|
98
|
+
try {
|
|
99
|
+
return await operation();
|
|
100
|
+
} finally {
|
|
101
|
+
restoreEnv(name, previous);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restoreEnv(name, value) {
|
|
106
|
+
if (value === undefined) delete process.env[name];
|
|
107
|
+
else process.env[name] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function summarize(name, result) {
|
|
111
|
+
return {
|
|
112
|
+
name,
|
|
113
|
+
status: result.status,
|
|
114
|
+
selected: result.selected,
|
|
115
|
+
fallback: result.fallback?.type ?? null,
|
|
116
|
+
confidence: result.confidence ?? null,
|
|
117
|
+
latency_ms: result.latency_ms,
|
|
118
|
+
execution: result.execution,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { loadConfig } from "../src/config.mjs";
|
|
3
|
-
import { decideBrowserStep } from "../src/browser.mjs";
|
|
4
|
-
import { routeRequest } from "../src/route.mjs";
|
|
5
|
-
import { superviseWork } from "../src/supervision.mjs";
|
|
6
|
-
|
|
7
|
-
const { config } = await loadConfig({ cwd: "/tmp/jev-no-config" });
|
|
8
|
-
assert.equal(config.features.browser_fast_path, false);
|
|
9
|
-
assert.equal(config.features.supervision, false);
|
|
10
|
-
assert.equal(config.features.context_filter, null);
|
|
11
|
-
|
|
12
|
-
const browser = await decideBrowserStep({ goal: "scroll down", observation: { scroll: { down: true } } }, {
|
|
13
|
-
provider: { name: "must-not-run", async decide() { throw new Error("disabled browser must not call Jev"); } },
|
|
14
|
-
});
|
|
15
|
-
assert.equal(browser.decision.fallback.type, "browser_fast_path_disabled");
|
|
16
|
-
|
|
17
|
-
const previousSupervision = process.env.JEV_SUPERVISION;
|
|
18
|
-
delete process.env.JEV_SUPERVISION;
|
|
19
|
-
try {
|
|
20
|
-
const supervision = await superviseWork({ provider: { async evaluate() { throw new Error("disabled supervision must not call Jev"); } } });
|
|
21
|
-
assert.equal(supervision.fallback.type, "disabled");
|
|
22
|
-
} finally {
|
|
23
|
-
if (previousSupervision === undefined) delete process.env.JEV_SUPERVISION;
|
|
24
|
-
else process.env.JEV_SUPERVISION = previousSupervision;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let calls = 0;
|
|
28
|
-
const route = await routeRequest({
|
|
29
|
-
intent: "inspect",
|
|
30
|
-
context: { messages: ["plain context"] },
|
|
31
|
-
capabilities: [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect context", risk: "low" }],
|
|
32
|
-
}, {
|
|
33
|
-
provider: { name: "default-route-provider", async decide() {
|
|
34
|
-
calls += 1;
|
|
35
|
-
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
36
|
-
} },
|
|
37
|
-
});
|
|
38
|
-
assert.equal(route.status, "selected");
|
|
39
|
-
assert.equal(calls, 1);
|
|
40
|
-
console.log(JSON.stringify({
|
|
41
|
-
ok: true,
|
|
42
|
-
browser: browser.decision.fallback.type,
|
|
43
|
-
supervision: "disabled",
|
|
44
|
-
context_filter: "disabled",
|
|
45
|
-
default_route_provider_calls: calls,
|
|
46
|
-
}, null, 2));
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { loadConfig } from "../src/config.mjs";
|
|
3
|
+
import { decideBrowserStep } from "../src/browser.mjs";
|
|
4
|
+
import { routeRequest } from "../src/route.mjs";
|
|
5
|
+
import { superviseWork } from "../src/supervision.mjs";
|
|
6
|
+
|
|
7
|
+
const { config } = await loadConfig({ cwd: "/tmp/jev-no-config" });
|
|
8
|
+
assert.equal(config.features.browser_fast_path, false);
|
|
9
|
+
assert.equal(config.features.supervision, false);
|
|
10
|
+
assert.equal(config.features.context_filter, null);
|
|
11
|
+
|
|
12
|
+
const browser = await decideBrowserStep({ goal: "scroll down", observation: { scroll: { down: true } } }, {
|
|
13
|
+
provider: { name: "must-not-run", async decide() { throw new Error("disabled browser must not call Jev"); } },
|
|
14
|
+
});
|
|
15
|
+
assert.equal(browser.decision.fallback.type, "browser_fast_path_disabled");
|
|
16
|
+
|
|
17
|
+
const previousSupervision = process.env.JEV_SUPERVISION;
|
|
18
|
+
delete process.env.JEV_SUPERVISION;
|
|
19
|
+
try {
|
|
20
|
+
const supervision = await superviseWork({ provider: { async evaluate() { throw new Error("disabled supervision must not call Jev"); } } });
|
|
21
|
+
assert.equal(supervision.fallback.type, "disabled");
|
|
22
|
+
} finally {
|
|
23
|
+
if (previousSupervision === undefined) delete process.env.JEV_SUPERVISION;
|
|
24
|
+
else process.env.JEV_SUPERVISION = previousSupervision;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let calls = 0;
|
|
28
|
+
const route = await routeRequest({
|
|
29
|
+
intent: "inspect",
|
|
30
|
+
context: { messages: ["plain context"] },
|
|
31
|
+
capabilities: [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect context", risk: "low" }],
|
|
32
|
+
}, {
|
|
33
|
+
provider: { name: "default-route-provider", async decide() {
|
|
34
|
+
calls += 1;
|
|
35
|
+
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
36
|
+
} },
|
|
37
|
+
});
|
|
38
|
+
assert.equal(route.status, "selected");
|
|
39
|
+
assert.equal(calls, 1);
|
|
40
|
+
console.log(JSON.stringify({
|
|
41
|
+
ok: true,
|
|
42
|
+
browser: browser.decision.fallback.type,
|
|
43
|
+
supervision: "disabled",
|
|
44
|
+
context_filter: "disabled",
|
|
45
|
+
default_route_provider_calls: calls,
|
|
46
|
+
}, null, 2));
|
|
@@ -1,98 +1,115 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { createInterface } from "node:readline";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
lines
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
10
|
+
const casesPath = join(await mkdtemp(join(tmpdir(), "jev-mcp-receipt-")), "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_REPLAY_CASES: casesPath },
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
const closed = new Promise((resolveExit) => child.once("close", resolveExit));
|
|
17
|
+
const lines = createInterface({ input: child.stdout });
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
lines.on("line", (line) => {
|
|
20
|
+
if (!line.trim()) return;
|
|
21
|
+
const message = JSON.parse(line);
|
|
22
|
+
const waiter = pending.get(message.id);
|
|
23
|
+
if (waiter) {
|
|
24
|
+
pending.delete(message.id);
|
|
25
|
+
waiter(message);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const initialized = await request(1, "initialize", {});
|
|
31
|
+
assert.equal(initialized.result.serverInfo.name, "jev-layer");
|
|
32
|
+
await notification("notifications/initialized", {});
|
|
33
|
+
const listed = await request(2, "tools/list", {});
|
|
34
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_route"));
|
|
35
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_model_route"));
|
|
36
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_shadow_compaction"));
|
|
37
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_record_execution"));
|
|
38
|
+
|
|
39
|
+
const routed = await request(3, "tools/call", {
|
|
40
|
+
name: "jev_route",
|
|
41
|
+
arguments: {
|
|
42
|
+
provider: "demo",
|
|
43
|
+
harness: "mcp-receipt-smoke",
|
|
44
|
+
intent: "read repository metadata",
|
|
45
|
+
actor_permissions: ["read"],
|
|
46
|
+
capabilities: [{
|
|
47
|
+
id: "read_metadata",
|
|
48
|
+
kind: "tool",
|
|
49
|
+
name: "Read metadata",
|
|
50
|
+
description: "Read repository metadata without modifying files",
|
|
51
|
+
permissions: ["read"],
|
|
52
|
+
risk: "low",
|
|
53
|
+
}],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const decision = routed.result.structuredContent;
|
|
57
|
+
assert.equal(decision.status, "selected");
|
|
58
|
+
assert.equal(decision.receipt.replay_persisted, true);
|
|
59
|
+
|
|
60
|
+
const recorded = await request(4, "tools/call", {
|
|
61
|
+
name: "jev_record_execution",
|
|
62
|
+
arguments: {
|
|
63
|
+
correlation_id: decision.correlation_id,
|
|
64
|
+
harness: "mcp-receipt-smoke",
|
|
65
|
+
capability_id: decision.selected,
|
|
66
|
+
status: "completed",
|
|
67
|
+
result: { observed: "metadata" },
|
|
68
|
+
exit_status: 0,
|
|
69
|
+
duration_ms: 3.2,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const receipt = recorded.result.structuredContent;
|
|
73
|
+
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
74
|
+
assert.equal(receipt.host.exit_status, 0);
|
|
75
|
+
assert.equal(receipt.host.duration_ms, 3.2);
|
|
76
|
+
|
|
77
|
+
const shadow = await request(5, "tools/call", {
|
|
78
|
+
name: "jev_shadow_compaction",
|
|
79
|
+
arguments: {
|
|
80
|
+
provider: "demo",
|
|
81
|
+
intent: "inspect context before compaction",
|
|
82
|
+
context: { messages: ["stale note", "Requirement: preserve /workspace/config.json"] },
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const shadowReport = shadow.result.structuredContent;
|
|
86
|
+
assert.equal(shadowReport.mode, "jev_shadow");
|
|
87
|
+
assert.equal(shadowReport.changed, false);
|
|
88
|
+
assert.equal(shadowReport.protected_count, 1);
|
|
89
|
+
|
|
90
|
+
const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
91
|
+
assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
|
|
92
|
+
console.log(JSON.stringify({ ok: true, cases_path: casesPath, receipt }, null, 2));
|
|
93
|
+
} finally {
|
|
94
|
+
child.kill();
|
|
95
|
+
await closed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function request(id, method, params) {
|
|
99
|
+
return new Promise((resolveResponse, reject) => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
pending.delete(id);
|
|
102
|
+
reject(new Error(`MCP request timed out: ${method}`));
|
|
103
|
+
}, 15_000);
|
|
104
|
+
pending.set(id, (message) => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
resolveResponse(message);
|
|
107
|
+
});
|
|
108
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function notification(method, params) {
|
|
113
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
114
|
+
return new Promise((resolveNotification) => setImmediate(resolveNotification));
|
|
115
|
+
}
|