jev-layer 0.1.0 → 0.2.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 +45 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +123 -126
- package/README.ru.md +138 -117
- package/README.zh-CN.md +137 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +56 -56
- package/bin/jev.mjs +213 -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 +22 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +250 -0
- package/docs/COMPATIBILITY.md +15 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
- package/docs/PROVIDERS.md +67 -0
- package/docs/SCHEMA-VERSIONING.md +96 -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 +150 -51
- package/integrations/hermes/plugin.yaml +10 -5
- package/integrations/hermes/schemas.py +89 -12
- package/integrations/omp/extension.js +105 -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 +162 -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 +113 -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 +571 -569
- package/src/cli.mjs +33 -33
- package/src/config.mjs +85 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +72 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +123 -0
- package/src/mcp-server.mjs +267 -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 +126 -126
- package/src/receipts.mjs +227 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +219 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +199 -199
- package/test/capability.test.mjs +54 -54
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +62 -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 +126 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +74 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
|
@@ -1,51 +1,150 @@
|
|
|
1
|
-
"""Hermes adapter for the standalone Jev layer.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
"""Full Hermes adapter for the standalone Jev decision layer.
|
|
2
|
+
|
|
3
|
+
Jev receives only host-supplied bounded choices. Hermes retains tool lookup,
|
|
4
|
+
permissions, approvals, native execution, retries, recovery, and final output.
|
|
5
|
+
The adapter is fail-open: transport or core errors become a Jev fallback and
|
|
6
|
+
never block Hermes's normal path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import threading
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .schemas import BROWSER_STEP, MODEL_ROUTE, RECORD_EXECUTION, ROUTE, SHADOW_COMPACTION, SUPERVISE
|
|
20
|
+
|
|
21
|
+
ROOT = Path(os.environ.get("JEV_LAYER_ROOT", Path(__file__).resolve().parents[2])).expanduser().resolve()
|
|
22
|
+
CORE = ROOT / "src" / "hermes-adapter.mjs"
|
|
23
|
+
_MAX_PENDING_DECISIONS = 64
|
|
24
|
+
_pending_decisions: OrderedDict[str, dict[str, Any]] = OrderedDict()
|
|
25
|
+
_pending_lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fallback(reason: str) -> str:
|
|
29
|
+
return json.dumps({
|
|
30
|
+
"schema_version": 1,
|
|
31
|
+
"status": "fallback",
|
|
32
|
+
"selected": None,
|
|
33
|
+
"reason": reason,
|
|
34
|
+
"fallback": {"type": "adapter_error", "reason": reason},
|
|
35
|
+
"execution": {"enabled": False, "status": "not_started"},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _runtime_env() -> dict[str, str]:
|
|
40
|
+
env = os.environ.copy()
|
|
41
|
+
# Resolve only through Hermes's profile-scoped store, then pass it only to
|
|
42
|
+
# this short-lived local Node adapter. It is never returned or logged.
|
|
43
|
+
try:
|
|
44
|
+
from agent.secret_scope import get_secret
|
|
45
|
+
openrouter_key = get_secret("OPENROUTER_API_KEY")
|
|
46
|
+
if openrouter_key:
|
|
47
|
+
env["OPENROUTER_API_KEY"] = openrouter_key
|
|
48
|
+
except Exception:
|
|
49
|
+
# The core remains fail-open when the optional provider credential is unavailable.
|
|
50
|
+
pass
|
|
51
|
+
# Keep replay evidence in the active Hermes profile, never in the source checkout.
|
|
52
|
+
env.setdefault("JEV_REPLAY_CASES", str(Path(env.get("HERMES_HOME", "~/.hermes")).expanduser() / "jev-layer" / "replay" / "cases.jsonl"))
|
|
53
|
+
return env
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _call_core(operation: str, args: dict, *, decision: dict | None = None) -> dict:
|
|
57
|
+
envelope: dict[str, Any] = {"operation": operation, "args": args}
|
|
58
|
+
if decision is not None:
|
|
59
|
+
envelope["decision"] = decision
|
|
60
|
+
try:
|
|
61
|
+
completed = subprocess.run(
|
|
62
|
+
[os.environ.get("JEV_NODE", "node"), str(CORE)],
|
|
63
|
+
input=json.dumps(envelope) + "\n", capture_output=True, text=True,
|
|
64
|
+
timeout=float(os.environ.get("JEV_LAYER_TIMEOUT_S", "5")), cwd=ROOT,
|
|
65
|
+
env=_runtime_env(), check=False,
|
|
66
|
+
)
|
|
67
|
+
if completed.returncode != 0:
|
|
68
|
+
return json.loads(_fallback(completed.stderr.strip() or f"core exited with status {completed.returncode}"))
|
|
69
|
+
line = next((line for line in completed.stdout.splitlines() if line.strip()), "")
|
|
70
|
+
if not line:
|
|
71
|
+
return json.loads(_fallback("core returned no response"))
|
|
72
|
+
result = json.loads(line)
|
|
73
|
+
return result if isinstance(result, dict) else json.loads(_fallback("core returned non-object response"))
|
|
74
|
+
except Exception as error:
|
|
75
|
+
return json.loads(_fallback(str(error)))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remember(decision: dict) -> None:
|
|
79
|
+
correlation_id = decision.get("correlation_id")
|
|
80
|
+
if not isinstance(correlation_id, str) or not correlation_id:
|
|
81
|
+
return
|
|
82
|
+
with _pending_lock:
|
|
83
|
+
_pending_decisions[correlation_id] = decision
|
|
84
|
+
_pending_decisions.move_to_end(correlation_id)
|
|
85
|
+
while len(_pending_decisions) > _MAX_PENDING_DECISIONS:
|
|
86
|
+
_pending_decisions.popitem(last=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _take(correlation_id: str) -> dict | None:
|
|
90
|
+
with _pending_lock:
|
|
91
|
+
return _pending_decisions.pop(correlation_id, None)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _route(args: dict, operation: str = "route") -> str:
|
|
95
|
+
request = dict(args)
|
|
96
|
+
request.setdefault("schema_version", 1)
|
|
97
|
+
request.setdefault("harness", "hermes")
|
|
98
|
+
result = _call_core(operation, request)
|
|
99
|
+
if result.get("status") in {"selected", "fallback", "no_decision", "needs_confirmation"} and result.get("correlation_id"):
|
|
100
|
+
_remember(result)
|
|
101
|
+
result.pop("_jev_request", None)
|
|
102
|
+
return json.dumps(result)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def jev_route(args: dict, **kwargs) -> str:
|
|
106
|
+
"""Choose from a closed Hermes-owned candidate set; never execute it."""
|
|
107
|
+
return _route(args)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def jev_browser_step(args: dict, **kwargs) -> str:
|
|
111
|
+
"""Choose one bounded browser action; Hermes owns observation, approval and execution."""
|
|
112
|
+
return _route(args, operation="browser_step")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def jev_model_route(args: dict, **kwargs) -> str:
|
|
116
|
+
"""Recommend a declared model profile; this never changes Hermes's model route."""
|
|
117
|
+
return _route(args, operation="model_route")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def jev_shadow_compaction(args: dict, **kwargs) -> str:
|
|
121
|
+
"""Report Jev's conservative keep/drop candidates without mutating host context."""
|
|
122
|
+
return json.dumps(_call_core("shadow_compaction", dict(args)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def jev_supervise(args: dict, **kwargs) -> str:
|
|
126
|
+
"""Return a bounded work-state judgment; Hermes maps it to its own next action."""
|
|
127
|
+
request = dict(args)
|
|
128
|
+
request.setdefault("harness", "hermes")
|
|
129
|
+
return json.dumps(_call_core("supervise", request))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def jev_record_execution(args: dict, **kwargs) -> str:
|
|
133
|
+
"""Append an execution receipt for a decision made in this Hermes process."""
|
|
134
|
+
correlation_id = args.get("correlation_id")
|
|
135
|
+
if not isinstance(correlation_id, str) or not correlation_id:
|
|
136
|
+
return _fallback("correlation_id is required")
|
|
137
|
+
decision = _take(correlation_id)
|
|
138
|
+
if decision is None:
|
|
139
|
+
return _fallback("unknown or expired Jev correlation_id; host execution remains authoritative")
|
|
140
|
+
receipt = _call_core("record_execution", dict(args), decision=decision)
|
|
141
|
+
return json.dumps(receipt)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def register(ctx):
|
|
145
|
+
ctx.register_tool(name="jev_route", toolset="jev_layer", schema=ROUTE, handler=jev_route)
|
|
146
|
+
ctx.register_tool(name="jev_model_route", toolset="jev_layer", schema=MODEL_ROUTE, handler=jev_model_route)
|
|
147
|
+
ctx.register_tool(name="jev_shadow_compaction", toolset="jev_layer", schema=SHADOW_COMPACTION, handler=jev_shadow_compaction)
|
|
148
|
+
ctx.register_tool(name="jev_record_execution", toolset="jev_layer", schema=RECORD_EXECUTION, handler=jev_record_execution)
|
|
149
|
+
ctx.register_tool(name="jev_supervise", toolset="jev_layer", schema=SUPERVISE, handler=jev_supervise)
|
|
150
|
+
ctx.register_tool(name="jev_browser_step", toolset="jev_layer", schema=BROWSER_STEP, handler=jev_browser_step)
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
name: jev-layer
|
|
2
|
-
version: 0.1.0
|
|
3
|
-
description:
|
|
4
|
-
provides_tools:
|
|
5
|
-
- jev_route
|
|
1
|
+
name: jev-layer
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
description: Full host-owned Jev routing, receipts, supervision, shadow compaction, model routing, and browser decisions
|
|
4
|
+
provides_tools:
|
|
5
|
+
- jev_route
|
|
6
|
+
- jev_model_route
|
|
7
|
+
- jev_shadow_compaction
|
|
8
|
+
- jev_record_execution
|
|
9
|
+
- jev_supervise
|
|
10
|
+
- jev_browser_step
|
|
@@ -1,12 +1,89 @@
|
|
|
1
|
-
ROUTE = {
|
|
2
|
-
"type": "object",
|
|
3
|
-
"required": ["intent", "capabilities"],
|
|
4
|
-
"properties": {
|
|
5
|
-
"intent": {"type": "string"},
|
|
6
|
-
"harness": {"type": "string"},
|
|
7
|
-
"context": {"type": "object"},
|
|
8
|
-
"actor_permissions": {"type": "array", "items": {"type": "string"}},
|
|
9
|
-
"capabilities": {"type": "array", "items": {"type": "object"}},
|
|
10
|
-
"policy": {"type": "object"},
|
|
11
|
-
|
|
12
|
-
}
|
|
1
|
+
ROUTE = {
|
|
2
|
+
"type": "object",
|
|
3
|
+
"required": ["intent", "capabilities"],
|
|
4
|
+
"properties": {
|
|
5
|
+
"intent": {"type": "string"},
|
|
6
|
+
"harness": {"type": "string"},
|
|
7
|
+
"context": {"type": "object"},
|
|
8
|
+
"actor_permissions": {"type": "array", "items": {"type": "string"}},
|
|
9
|
+
"capabilities": {"type": "array", "items": {"type": "object"}},
|
|
10
|
+
"policy": {"type": "object"},
|
|
11
|
+
"provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
|
|
12
|
+
"engine": {"type": "string", "enum": ["native", "jevrouter"]},
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
RECORD_EXECUTION = {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["correlation_id", "status"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"correlation_id": {"type": "string"},
|
|
21
|
+
"harness": {"type": "string"},
|
|
22
|
+
"capability_id": {"type": "string"},
|
|
23
|
+
"status": {"type": "string", "enum": ["completed", "failed", "not_started"]},
|
|
24
|
+
"result": {},
|
|
25
|
+
"error": {},
|
|
26
|
+
"browser": {"type": "object"},
|
|
27
|
+
"exit_status": {"type": ["integer", "null"]},
|
|
28
|
+
"duration_ms": {"type": ["number", "null"]},
|
|
29
|
+
"started_at": {"type": ["string", "null"]},
|
|
30
|
+
"completed_at": {"type": ["string", "null"]},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
MODEL_ROUTE = {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"required": ["intent", "models"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"intent": {"type": "string"},
|
|
39
|
+
"context": {"type": "object"},
|
|
40
|
+
"models": {"type": "array", "minItems": 1, "items": {"type": "object"}},
|
|
41
|
+
"harness": {"type": "string"},
|
|
42
|
+
"policy": {"type": "object"},
|
|
43
|
+
"provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
SHADOW_COMPACTION = {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["intent", "context"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"intent": {"type": "string"},
|
|
52
|
+
"context": {"type": "object"},
|
|
53
|
+
"provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
|
|
54
|
+
"batch_size": {"type": "integer", "minimum": 1, "maximum": 8},
|
|
55
|
+
"keep_threshold": {"type": "number", "minimum": 0, "maximum": 1},
|
|
56
|
+
"min_confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
SUPERVISE = {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["job", "observation"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"job": {"type": "object"},
|
|
65
|
+
"observation": {"type": "object"},
|
|
66
|
+
"evidence": {"type": "object"},
|
|
67
|
+
"harness": {"type": "string"},
|
|
68
|
+
"actor_permissions": {"type": "array", "items": {"type": "string"}},
|
|
69
|
+
"policy": {"type": "object"},
|
|
70
|
+
"attempts": {"type": "integer", "minimum": 0},
|
|
71
|
+
"provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
|
|
72
|
+
"enabled": {"type": "boolean"},
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
BROWSER_STEP = {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": ["goal", "observation"],
|
|
79
|
+
"properties": {
|
|
80
|
+
"goal": {"type": "string"},
|
|
81
|
+
"harness": {"type": "string"},
|
|
82
|
+
"start_url": {"type": ["string", "null"]},
|
|
83
|
+
"observation": {"type": "object"},
|
|
84
|
+
"progress": {"type": "object"},
|
|
85
|
+
"policy": {"type": "object"},
|
|
86
|
+
"provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
|
|
87
|
+
"enabled": {"type": "boolean"},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const PACKAGE_ROOT = process.env.JEV_LAYER_ROOT
|
|
6
|
-
? resolve(process.env.JEV_LAYER_ROOT)
|
|
7
|
-
: fileURLToPath(new URL("../..", import.meta.url));
|
|
8
|
-
const CORE = resolve(PACKAGE_ROOT, "src/cli.mjs");
|
|
9
|
-
export default function jevLayerExtension(pi) {
|
|
10
|
-
const z = pi.zod;
|
|
11
|
-
pi.registerTool({
|
|
12
|
-
name: "jev_route",
|
|
13
|
-
label: "Jev route",
|
|
14
|
-
description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
|
|
15
|
-
parameters: z.object({
|
|
16
|
-
intent: z.string(),
|
|
17
|
-
harness: z.string().optional(),
|
|
18
|
-
context: z.unknown().optional(),
|
|
19
|
-
actor_permissions: z.array(z.string()).optional(),
|
|
20
|
-
capabilities: z.array(z.unknown()),
|
|
21
|
-
policy: z.unknown().optional(),
|
|
22
|
-
}),
|
|
23
|
-
async execute(_toolCallId, params, signal) {
|
|
24
|
-
const decision = await runCore({ ...params, harness: params.harness ?? "omp" }, signal);
|
|
25
|
-
return {
|
|
26
|
-
content: [{ type: "text", text: JSON.stringify(decision) }],
|
|
27
|
-
details: decision,
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function runCore(payload, signal) {
|
|
34
|
-
if (globalThis.Bun?.spawn) return runCoreWithBun(payload, signal);
|
|
35
|
-
return runCoreWithNode(payload, signal);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function runCoreWithBun(payload, signal) {
|
|
39
|
-
const child = Bun.spawn([process.env.JEV_NODE ?? "node", CORE
|
|
40
|
-
cwd: process.cwd(),
|
|
41
|
-
env: process.env,
|
|
42
|
-
stdin: "pipe",
|
|
43
|
-
stdout: "pipe",
|
|
44
|
-
stderr: "pipe",
|
|
45
|
-
});
|
|
46
|
-
const abort = () => child.kill("SIGTERM");
|
|
47
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
-
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
49
|
-
child.stdin.end();
|
|
50
|
-
const [stdout, stderr, code] = await Promise.all([
|
|
51
|
-
new Response(child.stdout).text(),
|
|
52
|
-
new Response(child.stderr).text(),
|
|
53
|
-
child.exited,
|
|
54
|
-
]);
|
|
55
|
-
signal?.removeEventListener("abort", abort);
|
|
56
|
-
return parseCoreOutput(stdout, stderr, code);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function runCoreWithNode(payload, signal) {
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
const child = spawn(process.env.JEV_NODE ?? "node", [CORE
|
|
62
|
-
cwd: process.cwd(),
|
|
63
|
-
env: process.env,
|
|
64
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
-
});
|
|
66
|
-
let stdout = "";
|
|
67
|
-
let stderr = "";
|
|
68
|
-
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
69
|
-
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
70
|
-
const abort = () => child.kill("SIGTERM");
|
|
71
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
72
|
-
child.on("error", (error) => resolve(fallback(error.message)));
|
|
73
|
-
child.on("close", (code) => {
|
|
74
|
-
signal?.removeEventListener("abort", abort);
|
|
75
|
-
resolve(parseCoreOutput(stdout, stderr, code));
|
|
76
|
-
});
|
|
77
|
-
child.stdin.end(`${JSON.stringify(payload)}\n`);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseCoreOutput(stdout, stderr, code) {
|
|
82
|
-
if (code !== 0) return fallback(stderr.trim() || `core exited with status ${code}`);
|
|
83
|
-
const lines = stdout.split(/\r?\n/);
|
|
84
|
-
for (const line of lines) {
|
|
85
|
-
const jsonStart = line.indexOf("{");
|
|
86
|
-
if (jsonStart < 0) continue;
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(line.slice(jsonStart));
|
|
89
|
-
} catch {
|
|
90
|
-
// Ignore non-decision lines emitted by a host runtime wrapper.
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return fallback("core returned no valid JSON decision");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function fallback(reason) {
|
|
97
|
-
return {
|
|
98
|
-
schema_version: 1,
|
|
99
|
-
status: "fallback",
|
|
100
|
-
selected: null,
|
|
101
|
-
reason,
|
|
102
|
-
fallback: { type: "adapter_error", reason },
|
|
103
|
-
execution: { enabled: false, status: "not_started" },
|
|
104
|
-
};
|
|
105
|
-
}
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = process.env.JEV_LAYER_ROOT
|
|
6
|
+
? resolve(process.env.JEV_LAYER_ROOT)
|
|
7
|
+
: fileURLToPath(new URL("../..", import.meta.url));
|
|
8
|
+
const CORE = resolve(PACKAGE_ROOT, "src/cli.mjs");
|
|
9
|
+
export default function jevLayerExtension(pi) {
|
|
10
|
+
const z = pi.zod;
|
|
11
|
+
pi.registerTool({
|
|
12
|
+
name: "jev_route",
|
|
13
|
+
label: "Jev route",
|
|
14
|
+
description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
|
|
15
|
+
parameters: z.object({
|
|
16
|
+
intent: z.string(),
|
|
17
|
+
harness: z.string().optional(),
|
|
18
|
+
context: z.unknown().optional(),
|
|
19
|
+
actor_permissions: z.array(z.string()).optional(),
|
|
20
|
+
capabilities: z.array(z.unknown()),
|
|
21
|
+
policy: z.unknown().optional(),
|
|
22
|
+
}),
|
|
23
|
+
async execute(_toolCallId, params, signal) {
|
|
24
|
+
const decision = await runCore({ ...params, harness: params.harness ?? "omp" }, signal);
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: JSON.stringify(decision) }],
|
|
27
|
+
details: decision,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runCore(payload, signal) {
|
|
34
|
+
if (globalThis.Bun?.spawn) return runCoreWithBun(payload, signal);
|
|
35
|
+
return runCoreWithNode(payload, signal);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function runCoreWithBun(payload, signal) {
|
|
39
|
+
const child = Bun.spawn([process.env.JEV_NODE ?? "node", CORE], {
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
env: process.env,
|
|
42
|
+
stdin: "pipe",
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
});
|
|
46
|
+
const abort = () => child.kill("SIGTERM");
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
49
|
+
child.stdin.end();
|
|
50
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
51
|
+
new Response(child.stdout).text(),
|
|
52
|
+
new Response(child.stderr).text(),
|
|
53
|
+
child.exited,
|
|
54
|
+
]);
|
|
55
|
+
signal?.removeEventListener("abort", abort);
|
|
56
|
+
return parseCoreOutput(stdout, stderr, code);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runCoreWithNode(payload, signal) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const child = spawn(process.env.JEV_NODE ?? "node", [CORE], {
|
|
62
|
+
cwd: process.cwd(),
|
|
63
|
+
env: process.env,
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
+
});
|
|
66
|
+
let stdout = "";
|
|
67
|
+
let stderr = "";
|
|
68
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
69
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
70
|
+
const abort = () => child.kill("SIGTERM");
|
|
71
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
72
|
+
child.on("error", (error) => resolve(fallback(error.message)));
|
|
73
|
+
child.on("close", (code) => {
|
|
74
|
+
signal?.removeEventListener("abort", abort);
|
|
75
|
+
resolve(parseCoreOutput(stdout, stderr, code));
|
|
76
|
+
});
|
|
77
|
+
child.stdin.end(`${JSON.stringify(payload)}\n`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseCoreOutput(stdout, stderr, code) {
|
|
82
|
+
if (code !== 0) return fallback(stderr.trim() || `core exited with status ${code}`);
|
|
83
|
+
const lines = stdout.split(/\r?\n/);
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const jsonStart = line.indexOf("{");
|
|
86
|
+
if (jsonStart < 0) continue;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(line.slice(jsonStart));
|
|
89
|
+
} catch {
|
|
90
|
+
// Ignore non-decision lines emitted by a host runtime wrapper.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return fallback("core returned no valid JSON decision");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fallback(reason) {
|
|
97
|
+
return {
|
|
98
|
+
schema_version: 1,
|
|
99
|
+
status: "fallback",
|
|
100
|
+
selected: null,
|
|
101
|
+
reason,
|
|
102
|
+
fallback: { type: "adapter_error", reason },
|
|
103
|
+
execution: { enabled: false, status: "not_started" },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Template harness adapter
|
|
2
|
-
|
|
3
|
-
Copy `adapter.mjs` to `integrations/<harness>/` and replace the injected callbacks with the harness's native implementations:
|
|
4
|
-
|
|
5
|
-
- `jevRoute(request)`: call `jev_route` through the existing stdio MCP or CLI boundary;
|
|
6
|
-
- `jevRecordExecution(args)`: call `jev_record_execution` with the original `correlation_id`;
|
|
7
|
-
- `approve(capabilityId, decision, request)`: delegate to the host's normal approval UI/policy;
|
|
8
|
-
- `executeNative(capabilityId, request, decision)`: resolve and execute the id through the host registry.
|
|
9
|
-
|
|
10
|
-
The template intentionally does not discover arbitrary tools, grant permissions, retry actions, or spawn workers. Jev failures return `native_fallback`; the host should continue its normal path. Copy the template only after adding a harness-specific smoke fixture and compatibility entry.
|
|
1
|
+
# Template harness adapter
|
|
2
|
+
|
|
3
|
+
Copy `adapter.mjs` to `integrations/<harness>/` and replace the injected callbacks with the harness's native implementations:
|
|
4
|
+
|
|
5
|
+
- `jevRoute(request)`: call `jev_route` through the existing stdio MCP or CLI boundary;
|
|
6
|
+
- `jevRecordExecution(args)`: call `jev_record_execution` with the original `correlation_id`;
|
|
7
|
+
- `approve(capabilityId, decision, request)`: delegate to the host's normal approval UI/policy;
|
|
8
|
+
- `executeNative(capabilityId, request, decision)`: resolve and execute the id through the host registry.
|
|
9
|
+
|
|
10
|
+
The template intentionally does not discover arbitrary tools, grant permissions, retry actions, or spawn workers. Jev failures return `native_fallback`; the host should continue its normal path. Copy the template only after adding a harness-specific smoke fixture and compatibility entry.
|