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
package/SECURITY.md ADDED
@@ -0,0 +1,56 @@
1
+ # Security policy
2
+
3
+ [English](README.md) | [Русский](README.ru.md) | [简体中文](README.zh-CN.md)
4
+
5
+ ## Scope and trust model
6
+
7
+ jev-layer is a routing and evidence layer, not a security boundary. It can return a bounded recommendation and persist a receipt; it does not grant permissions, execute host capabilities, approve actions, isolate a process, or validate that a provider is truthful.
8
+
9
+ The host remains the source of truth for:
10
+
11
+ - capability availability and permissions;
12
+ - approval and confirmation decisions;
13
+ - native execution, retries, recovery, and cancellation;
14
+ - final results and side effects.
15
+
16
+ Adapters must fail open to the host's normal path when Jev is disabled, unavailable, inconclusive, or returns an error. A Jev response must never be treated as authorization by itself.
17
+
18
+ ## API keys and credentials
19
+
20
+ - Keep provider keys in process environment variables or user-owned secret storage.
21
+ - Never commit `.env` files, API keys, bearer tokens, private keys, provider headers, or copied secret-bearing requests.
22
+ - Do not put credentials in `context`, capabilities, prompts, replay fixtures, issue reports, or receipts.
23
+ - Use the secret-free `demo` provider for local tests and ordinary pull requests.
24
+ - If a credential is exposed, revoke it immediately, remove it from active systems, and report the incident privately.
25
+
26
+ Supported provider environment names include `OPENROUTER_API_KEY` and `TYPESAFE_API_KEY`. The names may appear in configuration examples; their values must not.
27
+
28
+ ## Logs, receipts, and replay data
29
+
30
+ Routing cases and execution receipts can contain capability names, prompts or bounded context, host results, errors, URLs, timing, and provider metadata. Treat `.jev/replay/cases.jsonl` and CI logs as potentially sensitive.
31
+
32
+ Before sharing logs or fixtures:
33
+
34
+ 1. remove API keys, tokens, cookies, authorization headers, private paths, and personal data;
35
+ 2. remove sensitive prompt/context and host output;
36
+ 3. retain only the smallest correlation and status fields needed to reproduce the issue.
37
+
38
+ Receipt fields are bounded, but bounded data can still be confidential. Do not use receipts as an authorization ledger.
39
+
40
+ ## Prompt injection and provider limitations
41
+
42
+ Provider output is untrusted data. Prompt injection, malicious capability descriptions, misleading browser text, compromised providers, and incorrect confidence values can cause a bad recommendation. jev-layer does not claim to detect or defeat prompt injection.
43
+
44
+ The host must validate selected ids against its current registry, enforce permissions and approval, and reject stale, unavailable, or malformed selections. The browser fast-path is experimental, opt-in, and must not be used as a security control.
45
+
46
+ ## Reporting a vulnerability
47
+
48
+ Do not open a public issue for an unpatched vulnerability or include secrets in a report. Use a private GitHub Security Advisory for the repository when that channel is available. If it is not available, contact the project maintainers through the private contact channel listed by the repository owner and provide:
49
+
50
+ - affected version or commit;
51
+ - minimal reproduction;
52
+ - impact and required permissions;
53
+ - whether credentials or personal data were exposed;
54
+ - a safe contact for coordinated follow-up.
55
+
56
+ We will acknowledge a report when practicable, validate the impact, coordinate a fix or mitigation, and publish a release note after a fix is available. Please allow maintainers reasonable time for coordinated disclosure.
package/bin/jev.mjs ADDED
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { configuredProvider, loadConfig, pathExists } from "../src/config.mjs";
6
+
7
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const [command = "help", ...argv] = process.argv.slice(2);
9
+
10
+ try {
11
+ if (command === "mcp") {
12
+ await import("../src/mcp-server.mjs");
13
+ } else if (command === "cli") {
14
+ await import("../src/cli.mjs");
15
+ } else if (command === "install") {
16
+ await install(argv);
17
+ } else if (command === "init") {
18
+ await init(argv);
19
+ } else if (command === "add") {
20
+ await add(argv);
21
+ } else if (command === "doctor") {
22
+ await doctor(argv);
23
+ } else {
24
+ printHelp();
25
+ if (command !== "help" && command !== "--help" && command !== "-h") process.exitCode = 1;
26
+ }
27
+ } catch (error) {
28
+ console.error(`jev: ${error instanceof Error ? error.message : String(error)}`);
29
+ process.exitCode = 1;
30
+ }
31
+
32
+ async function install(argv) {
33
+ const options = parseOptions(argv);
34
+ const project = projectPath(options);
35
+ const result = await writeInitialConfig(project, options.force);
36
+ console.log(`jev-layer ${result.created ? "initialized" : "already initialized"} at ${project}`);
37
+ console.log("Runtime dependencies: none. Node.js >= 20 is required.");
38
+ console.log("Next: jev add <omp|hermes|codex|generic> && jev doctor");
39
+ }
40
+
41
+ async function init(argv) {
42
+ const options = parseOptions(argv);
43
+ const project = projectPath(options);
44
+ const result = await writeInitialConfig(project, options.force);
45
+ console.log(JSON.stringify({ ok: true, project, created: result.created, config: result.configPath, env_example: result.envPath }, null, 2));
46
+ }
47
+
48
+ async function add(argv) {
49
+ const harness = argv[0];
50
+ if (!harness || ["--help", "-h"].includes(harness)) {
51
+ console.error("Usage: jev add <omp|hermes|codex|generic> [--project DIR] [--command CMD]");
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ const options = parseOptions(argv.slice(1));
56
+ const project = projectPath(options);
57
+ const { config } = await loadConfig({ cwd: project });
58
+ const provider = configuredProvider(config, options.provider);
59
+ const commandName = options.command ?? process.env.JEV_COMMAND ?? "jev";
60
+ const server = {
61
+ type: "stdio",
62
+ command: commandName,
63
+ args: ["mcp"],
64
+ cwd: ".",
65
+ env: { JEV_LAYER_PROVIDER: provider },
66
+ };
67
+
68
+ let destination;
69
+ let mode = "snippet";
70
+ if (harness === "omp") {
71
+ destination = await addOmp(project, server);
72
+ mode = destination.endsWith(".omp/mcp.json") ? "config" : "snippet";
73
+ } else if (harness === "codex") {
74
+ destination = await addCodex(project, server);
75
+ mode = destination.endsWith(".codex/config.toml") ? "config" : "snippet";
76
+ } else if (harness === "hermes") {
77
+ destination = await writeSnippet(project, "hermes", hermesSnippet(server));
78
+ } else if (harness === "generic" || harness === "mcp") {
79
+ destination = await writeSnippet(project, "generic", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
80
+ } else {
81
+ throw new Error(`unsupported harness: ${harness}`);
82
+ }
83
+
84
+ console.log(JSON.stringify({ ok: true, harness, provider, destination, mode, command: commandName }, null, 2));
85
+ if (mode === "snippet") console.log("Merge this adapter snippet into the harness configuration; existing files were not overwritten.");
86
+ }
87
+
88
+ async function doctor(argv) {
89
+ const options = parseOptions(argv);
90
+ const project = projectPath(options);
91
+ const { config, path: configPath, error: configError } = await loadConfig({ cwd: project });
92
+ const provider = configuredProvider(config, options.provider);
93
+ const checks = [];
94
+ const fail = (name, detail) => checks.push({ name, status: "error", detail });
95
+ const pass = (name, detail) => checks.push({ name, status: "ok", detail });
96
+ const warn = (name, detail) => checks.push({ name, status: "warning", detail });
97
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
98
+
99
+ if (nodeMajor >= 20) pass("node", process.versions.node);
100
+ else fail("node", `Node.js >= 20 required; found ${process.versions.node}`);
101
+ for (const relative of ["src/route.mjs", "src/cli.mjs", "src/mcp-server.mjs", "bin/jev.mjs"]) {
102
+ if (await pathExists(join(PACKAGE_ROOT, relative))) pass(`package:${relative}`, "present");
103
+ else fail(`package:${relative}`, "missing from installation");
104
+ }
105
+ if (configError) fail("config", configError);
106
+ else if (await pathExists(configPath)) pass("config", configPath);
107
+ else warn("config", `not initialized at ${configPath}; run jev init`);
108
+
109
+ if (!["demo", "openrouter", "typesafe"].includes(provider)) fail("provider", `unsupported provider ${provider}`);
110
+ else pass("provider", provider);
111
+ if (provider === "openrouter" || provider === "typesafe") {
112
+ const keyEnv = config.providers?.[provider]?.api_key_env ?? (provider === "openrouter" ? "OPENROUTER_API_KEY" : "TYPESAFE_API_KEY");
113
+ if (process.env[keyEnv]) pass(`secret:${keyEnv}`, "configured through environment");
114
+ else fail(`secret:${keyEnv}`, `missing; export ${keyEnv} before using ${provider}`);
115
+ } else {
116
+ pass("provider-secret", "not required for demo provider");
117
+ }
118
+ if (process.env.JEV_LAYER_ENABLED === "0") warn("fail-open", "JEV_LAYER_ENABLED=0; routing will return fallback decisions");
119
+
120
+ const errors = checks.filter((check) => check.status === "error");
121
+ console.log(JSON.stringify({ ok: errors.length === 0, project, package_root: PACKAGE_ROOT, checks }, null, 2));
122
+ if (errors.length) process.exitCode = 1;
123
+ }
124
+
125
+ async function writeInitialConfig(project, force = false) {
126
+ const state = join(project, ".jev");
127
+ await mkdir(state, { recursive: true });
128
+ const configPath = join(state, "config.json");
129
+ const envPath = join(state, "providers.env.example");
130
+ const config = await readPackageFile("config/jev.example.json");
131
+ const envExample = await readPackageFile("config/providers.env.example");
132
+ const configCreated = await writeIfAllowed(configPath, config, force);
133
+ const envCreated = await writeIfAllowed(envPath, envExample, force);
134
+ return { created: configCreated || envCreated, configPath, envPath };
135
+ }
136
+
137
+ async function addOmp(project, server) {
138
+ const target = join(project, ".omp", "mcp.json");
139
+ if (!(await pathExists(target))) {
140
+ await writeJson(target, { "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json", mcpServers: { jev: server } });
141
+ return target;
142
+ }
143
+ try {
144
+ const parsed = JSON.parse(await readFile(target, "utf8"));
145
+ parsed.mcpServers = { ...(parsed.mcpServers ?? {}), jev: server };
146
+ await writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
147
+ return target;
148
+ } catch {
149
+ return writeSnippet(project, "omp", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
150
+ }
151
+ }
152
+
153
+ async function addCodex(project, server) {
154
+ const target = join(project, ".codex", "config.toml");
155
+ if (!(await pathExists(target))) {
156
+ await mkdir(dirname(target), { recursive: true });
157
+ await writeFile(target, codexSnippet(server), "utf8");
158
+ return target;
159
+ }
160
+ return writeSnippet(project, "codex", codexSnippet(server), ".toml");
161
+ }
162
+
163
+ async function writeSnippet(project, name, content, extension = ".yaml") {
164
+ const destination = join(project, ".jev", "adapters", `${name}${extension}`);
165
+ await mkdir(dirname(destination), { recursive: true });
166
+ await writeFile(destination, content, "utf8");
167
+ return destination;
168
+ }
169
+
170
+ function codexSnippet(server) {
171
+ return `[mcp_servers.jev]\ncommand = ${JSON.stringify(server.command)}\nargs = ["mcp"]\ncwd = "."\n\n[mcp_servers.jev.env]\nJEV_LAYER_PROVIDER = ${JSON.stringify(server.env.JEV_LAYER_PROVIDER)}\n`;
172
+ }
173
+
174
+ function hermesSnippet(server) {
175
+ return `mcp_servers:\n jev:\n command: ${server.command}\n args:\n - mcp\n env:\n JEV_LAYER_PROVIDER: ${server.env.JEV_LAYER_PROVIDER}\n # Export OPENROUTER_API_KEY or TYPESAFE_API_KEY in the Hermes process environment.\n`;
176
+ }
177
+
178
+ async function writeJson(path, value) {
179
+ await mkdir(dirname(path), { recursive: true });
180
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
181
+ }
182
+
183
+ async function writeIfAllowed(path, content, force) {
184
+ if (!force && await pathExists(path)) return false;
185
+ await mkdir(dirname(path), { recursive: true });
186
+ await writeFile(path, content, "utf8");
187
+ return true;
188
+ }
189
+
190
+ async function readPackageFile(relative) {
191
+ return readFile(join(PACKAGE_ROOT, relative), "utf8");
192
+ }
193
+
194
+ function projectPath(options) {
195
+ return resolve(options.project ?? process.cwd());
196
+ }
197
+
198
+ function parseOptions(argv) {
199
+ const options = {};
200
+ for (let index = 0; index < argv.length; index += 1) {
201
+ const value = argv[index];
202
+ if (value === "--project") options.project = argv[++index];
203
+ else if (value === "--command") options.command = argv[++index];
204
+ else if (value === "--provider") options.provider = argv[++index];
205
+ else if (value === "--force") options.force = true;
206
+ else if (value === "--help" || value === "-h") options.help = true;
207
+ else throw new Error(`unknown option ${value}`);
208
+ }
209
+ return options;
210
+ }
211
+
212
+ function printHelp() {
213
+ console.log(`jev-layer portable setup\n\nCommands:\n jev install [--project DIR] initialize local state\n jev init [--project DIR] write .jev/config.json and env example\n jev add <harness> [options] add omp, hermes, codex, or generic MCP adapter\n jev doctor [--project DIR] check runtime, config, provider and secrets\n jev mcp run the stdio MCP server\n jev cli run the JSONL CLI adapter\n\nOptions:\n --project DIR target workspace; defaults to cwd\n --command CMD MCP command; defaults to jev\n --provider NAME override provider for generated config\n --force replace generated init files\n`);
214
+ }
@@ -0,0 +1,8 @@
1
+ [mcp_servers.jev]
2
+ command = "jev"
3
+ args = ["mcp"]
4
+ cwd = "."
5
+
6
+ [mcp_servers.jev.env]
7
+ JEV_LAYER_PROVIDER = "openrouter"
8
+ # Export OPENROUTER_API_KEY in the Codex process environment.
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "jev": {
4
+ "type": "stdio",
5
+ "command": "jev",
6
+ "args": ["mcp"],
7
+ "cwd": ".",
8
+ "env": {
9
+ "JEV_LAYER_PROVIDER": "demo"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ # Merge this block into the Hermes config under mcp_servers.
2
+ mcp_servers:
3
+ jev:
4
+ command: jev
5
+ args:
6
+ - mcp
7
+ env:
8
+ JEV_LAYER_PROVIDER: openrouter
9
+ # Export OPENROUTER_API_KEY in the Hermes process environment.
@@ -0,0 +1,22 @@
1
+ {
2
+ "schema_version": 1,
3
+ "provider": "demo",
4
+ "replay_cases": ".jev/replay/cases.jsonl",
5
+ "features": {
6
+ "browser_fast_path": false,
7
+ "supervision": false,
8
+ "context_filter": null
9
+ },
10
+ "providers": {
11
+ "openrouter": {
12
+ "api_key_env": "OPENROUTER_API_KEY",
13
+ "endpoint_env": "OPENROUTER_DECISIONS_ENDPOINT",
14
+ "model_env": "OPENROUTER_DECISIONS_MODEL"
15
+ },
16
+ "typesafe": {
17
+ "api_key_env": "TYPESAFE_API_KEY",
18
+ "endpoint_env": "TYPESAFE_ENDPOINT",
19
+ "model_env": "TYPESAFE_MODEL"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
3
+ "mcpServers": {
4
+ "jev": {
5
+ "type": "stdio",
6
+ "command": "jev",
7
+ "args": ["mcp"],
8
+ "cwd": ".",
9
+ "env": {
10
+ "JEV_LAYER_PROVIDER": "openrouter"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ # Copy the variables needed by the selected provider into the harness environment.
2
+ # Do not commit a file containing real values.
3
+
4
+ JEV_LAYER_PROVIDER=demo
5
+ # JEV_LAYER_PROVIDER=openrouter
6
+ # OPENROUTER_API_KEY=replace-in-your-shell-only
7
+ # OPENROUTER_DECISIONS_ENDPOINT=https://openrouter.ai/api/alpha/decisions
8
+ # OPENROUTER_DECISIONS_MODEL=typesafe/jev-1.13
9
+
10
+ # JEV_LAYER_PROVIDER=typesafe
11
+ # TYPESAFE_API_KEY=replace-in-your-shell-only
12
+ # TYPESAFE_ENDPOINT=https://api.typesafe.ai/v1/systemone
13
+ # TYPESAFE_MODEL=jev-latest
@@ -0,0 +1,15 @@
1
+ # Harness compatibility baseline
2
+
3
+ [English](../README.md) | [Русский](../README.ru.md) | [简体中文](../README.zh-CN.md)
4
+
5
+ This is the first-release compatibility baseline observed on 2026-09-19. It records the installed harness versions used for the release-preparation checks; it is not a claim that every provider/model combination was exercised.
6
+
7
+ | Harness | Version observed | Release-preparation coverage |
8
+ | --- | --- | --- |
9
+ | OMP | `18.2.6` | `omp --version`; OMP extension contract and host-owned execution path reviewed; Jev core smoke uses the same MCP/CLI contract. |
10
+ | Hermes | `0.21.3` (`upstream b675e6de`) | `hermes --version`; Python adapter syntax/contract reviewed; Jev core smoke uses the same CLI/MCP contract. |
11
+ | Codex CLI | `0.155.1` | `codex --version`; Codex MCP transport fixture and host-execution ownership path reviewed. The fixture does not invoke a Codex model. |
12
+
13
+ The adapters are thin and target the stable v1 contract documented in [CONTRIBUTING.md](../CONTRIBUTING.md) and [SCHEMA-VERSIONING.md](SCHEMA-VERSIONING.md). Full interactive harness/model/provider coverage is environment-specific and is not required for ordinary PR CI.
14
+
15
+ When a public release supports a different harness version, update this table, rerun the adapter/transport checks, and document any compatibility change in `CHANGELOG.md` before tagging.
@@ -0,0 +1,94 @@
1
+ # Schema and compatibility policy
2
+
3
+ [English](../README.md) | [Русский](../README.ru.md) | [简体中文](../README.zh-CN.md)
4
+
5
+ The current public contracts are **schema version 1**. The implementation already emits `schema_version: 1` for route requests, decision envelopes, routing cases, execution receipts, and supervision cases. A new public release must not silently change the meaning of those v1 fields.
6
+
7
+ ## Version rules
8
+
9
+ - **Patch release:** bug fixes and documentation changes; v1 meanings remain unchanged.
10
+ - **Minor release:** additive optional fields, new providers, new adapters, or new capability kinds that v1 consumers can ignore.
11
+ - **Major release:** removed/renamed required fields, changed field types or meanings, changed correlation semantics, or a host-approval contract change.
12
+ - Unknown fields must be ignored by readers. Writers must not emit a required field unless it is part of the declared schema version.
13
+ - New runtime behavior must remain opt-in and fail open unless the existing contract explicitly requires it.
14
+
15
+ A breaking schema requires a new schema version, migration notes, fixtures for both versions, and an adapter compatibility decision. Do not change v1 to avoid writing a migration.
16
+
17
+ ## MCP tools
18
+
19
+ The MCP server currently exposes these stable tool names:
20
+
21
+ - `jev_route`
22
+ - `jev_browser_step` (experimental, opt-in)
23
+ - `jev_supervise` (experimental, opt-in)
24
+ - `jev_record_execution`
25
+
26
+ Their input and structured output are v1. Additive optional properties are compatible. Renaming a tool, changing a required property, changing the meaning of a status, or changing who owns execution requires a new tool/schema version and adapter migration. Keep `tools/list`, `initialize`, and stdio JSON-RPC behavior backward compatible for v1 clients.
27
+
28
+ ## Routing decision schema
29
+
30
+ A v1 decision includes at least:
31
+
32
+ - `schema_version`
33
+ - `correlation_id`
34
+ - `status`
35
+ - `selected`
36
+ - `confidence`
37
+ - `probabilities`
38
+ - `reason`
39
+ - `candidates`
40
+ - `fallback`
41
+ - `execution` with host execution initially `not_started`
42
+ - `receipt`
43
+
44
+ The selected id is advisory until the host resolves it against its own capability registry and permissions. `correlation_id` is the join key for routing and execution records.
45
+
46
+ ## Execution receipts
47
+
48
+ A v1 `execution_receipt` is append-only JSONL data with:
49
+
50
+ - `record_type: "execution_receipt"`
51
+ - `schema_version: 1`
52
+ - `receipt_id`
53
+ - `correlation_id`
54
+ - `harness`
55
+ - the selected/candidate decision summary;
56
+ - Jev provider, latency, cost, and status;
57
+ - bounded host status/result/error plus optional exit and timing metadata.
58
+
59
+ The host must record `completed`, `failed`, or `not_started`. Receipts must not contain unbounded prompts, secrets, or raw credentials. Receipts are evidence of what the host reported; they do not grant permission or prove that Jev executed anything.
60
+
61
+ ## Replay cases
62
+
63
+ A v1 replay file is JSONL. Supported records include:
64
+
65
+ - `record_type: "routing_case"`
66
+ - `record_type: "execution_receipt"`
67
+ - `record_type: "supervision_case"`
68
+
69
+ Routing cases retain a sanitized request and a decision summary. Replay reads cases without calling host tools. New optional record fields are compatible; changing record type, correlation semantics, or the meaning of a recorded status requires a new replay schema and migration.
70
+
71
+ ## Adapter contract
72
+
73
+ The adapter contract is currently **v1** and is intentionally separate from provider/model versions. A v1 adapter must:
74
+
75
+ 1. submit a bounded candidate set;
76
+ 2. call `jev_route` or the equivalent CLI contract;
77
+ 3. preserve `correlation_id`;
78
+ 4. keep native execution and permissions in the host;
79
+ 5. preserve approval decisions;
80
+ 6. call `jev_record_execution` after host execution or denial;
81
+ 7. fail open to the native path when Jev is unavailable or inconclusive.
82
+
83
+ An adapter that cannot preserve these invariants is not v1-compatible. Record supported adapter versions in integration documentation and release checklists.
84
+
85
+ ## Compatibility review
86
+
87
+ For every release, review:
88
+
89
+ - v1 request/decision fixtures;
90
+ - MCP `tools/list` and `tools/call` smoke cases;
91
+ - receipt/replay round trips;
92
+ - all supported adapter examples;
93
+ - fail-open and approval-denial paths;
94
+ - package contents and translated documentation.
@@ -0,0 +1,39 @@
1
+ [
2
+ {
3
+ "id": "repo_search",
4
+ "kind": "mcp",
5
+ "name": "Repository search",
6
+ "description": "Search repository files and symbols without changing files",
7
+ "permissions": ["read"],
8
+ "risk": "low",
9
+ "execution": { "target": "host-native-repository-search" }
10
+ },
11
+ {
12
+ "id": "browser_inspect",
13
+ "kind": "mcp",
14
+ "name": "Browser inspection",
15
+ "description": "Open a webpage and inspect visible browser state",
16
+ "permissions": ["browser"],
17
+ "risk": "medium",
18
+ "execution": { "target": "host-native-browser" }
19
+ },
20
+ {
21
+ "id": "terminal_read",
22
+ "kind": "cli",
23
+ "name": "Read-only terminal",
24
+ "description": "Run a read-only shell command and return its output",
25
+ "permissions": ["terminal:read"],
26
+ "risk": "low",
27
+ "execution": { "target": "host-native-terminal" }
28
+ },
29
+ {
30
+ "id": "file_write",
31
+ "kind": "tool",
32
+ "name": "File writer",
33
+ "description": "Modify files in the repository",
34
+ "permissions": ["write"],
35
+ "risk": "high",
36
+ "execution": { "target": "host-native-write" },
37
+ "policy": { "requires_confirmation": true }
38
+ }
39
+ ]
@@ -0,0 +1,28 @@
1
+ {
2
+ "schema_version": 1,
3
+ "harness": "example",
4
+ "intent": "Find where the routing decision is implemented in this repository",
5
+ "context": {
6
+ "workspace": "jev-layer",
7
+ "api_key": "must-not-reach-provider"
8
+ },
9
+ "actor_permissions": ["read"],
10
+ "capabilities": [
11
+ {
12
+ "id": "repo_search",
13
+ "kind": "mcp",
14
+ "name": "Repository search",
15
+ "description": "Search repository files and symbols without changing files",
16
+ "permissions": ["read"],
17
+ "risk": "low"
18
+ },
19
+ {
20
+ "id": "file_write",
21
+ "kind": "tool",
22
+ "name": "File writer",
23
+ "description": "Modify files in the repository",
24
+ "permissions": ["write"],
25
+ "risk": "high"
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "jev-layer",
3
+ "version": "0.1.0",
4
+ "description": "Host-owned Jev capability routing through the standalone jev-layer core",
5
+ "author": { "name": "Local Jev Layer" },
6
+ "license": "MIT",
7
+ "keywords": ["jev", "routing", "decision", "mcp"],
8
+ "skills": "./skills/",
9
+ "mcpServers": "./.mcp.json",
10
+ "interface": {
11
+ "displayName": "Jev Layer",
12
+ "shortDescription": "Bounded capability decisions without host execution",
13
+ "longDescription": "Routes a closed candidate set and leaves approvals and execution to Codex.",
14
+ "developerName": "Local Jev Layer",
15
+ "category": "Developer Tools",
16
+ "capabilities": ["Decision routing"],
17
+ "defaultPrompt": ["Use Jev only for bounded capability routing; keep execution and approvals in Codex."]
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "jev": {
4
+ "type": "stdio",
5
+ "command": "node",
6
+ "args": ["./run-mcp.mjs"],
7
+ "cwd": ".",
8
+ "env_vars": ["JEV_LAYER_ROOT", "JEV_LAYER_PROVIDER", "OPENROUTER_API_KEY"]
9
+ }
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ When this adapter is enabled, use `jev_route` only for bounded closed-set capability routing. The tool returns an identifier; Codex owns all execution and approvals. On fallback, low confidence, malformed response, unavailable capability, or timeout, continue with normal Codex planning. Never send secrets or full transcripts to Jev.
@@ -0,0 +1,9 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const pluginRoot = dirname(fileURLToPath(import.meta.url));
5
+ const projectRoot = process.env.JEV_LAYER_ROOT
6
+ ? resolve(process.env.JEV_LAYER_ROOT)
7
+ : resolve(pluginRoot, "../..");
8
+
9
+ await import(resolve(projectRoot, "src/mcp-server.mjs"));
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: jev-route
3
+ description: Use Jev for one bounded capability choice when a closed candidate set is already available. Keep permissions, approvals, execution, and recovery in Codex.
4
+ ---
5
+
6
+ # Jev route
7
+
8
+ Use the `jev_route` MCP tool only when the request can be expressed as one bounded choice among supplied capabilities.
9
+
10
+ 1. Build a small candidate list from capabilities Codex can already execute.
11
+ 2. Include only non-secret intent, candidate metadata, and the minimum context needed.
12
+ 3. Call `jev_route` once.
13
+ 4. Treat `fallback`, `no_decision`, and `needs_confirmation` as normal outcomes.
14
+ 5. Execute `selected` only through the native Codex capability and its existing approval gate.
15
+ 6. Never interpret a Jev response as permission to bypass Codex policy.
16
+
17
+ Do not use Jev for open-ended planning, coding, research, recovery, or arbitrary tool execution.
@@ -0,0 +1,51 @@
1
+ """Hermes adapter for the standalone Jev layer.
2
+
3
+ This plugin exposes only a decision tool. The selected capability remains
4
+ owned and executed by Hermes through its normal approval/tool path.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from .schemas import ROUTE
13
+
14
+ ROOT = Path(os.environ.get("JEV_LAYER_ROOT", Path(__file__).resolve().parents[2])).expanduser().resolve()
15
+ CLI = ROOT / "src" / "cli.mjs"
16
+
17
+ def _fallback(reason: str) -> str:
18
+ return json.dumps({
19
+ "schema_version": 1,
20
+ "status": "fallback",
21
+ "selected": None,
22
+ "reason": reason,
23
+ "fallback": {"type": "adapter_error", "reason": reason},
24
+ "execution": {"enabled": False, "status": "not_started"},
25
+ })
26
+
27
+
28
+ def jev_route(args: dict, **kwargs) -> str:
29
+ """Return a bounded decision as JSON; never execute the selected target."""
30
+ try:
31
+ completed = subprocess.run(
32
+ [os.environ.get("JEV_NODE", "node"), str(CLI), "--provider", os.environ.get("JEV_LAYER_PROVIDER", "demo")],
33
+ input=json.dumps(args) + "\n",
34
+ capture_output=True,
35
+ text=True,
36
+ timeout=float(os.environ.get("JEV_LAYER_TIMEOUT_S", "3")),
37
+ check=False,
38
+ )
39
+ if completed.returncode != 0:
40
+ return _fallback(completed.stderr.strip() or f"core exited with status {completed.returncode}")
41
+ line = next((line for line in completed.stdout.splitlines() if line.strip()), "")
42
+ if not line:
43
+ return _fallback("core returned no decision")
44
+ json.loads(line)
45
+ return line
46
+ except Exception as error:
47
+ return _fallback(str(error))
48
+
49
+
50
+ def register(ctx):
51
+ ctx.register_tool(name="jev_route", toolset="jev_layer", schema=ROUTE, handler=jev_route)
@@ -0,0 +1,5 @@
1
+ name: jev-layer
2
+ version: 0.1.0
3
+ description: Host-owned Jev capability routing through the standalone jev-layer core
4
+ provides_tools:
5
+ - jev_route