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
package/src/cli.mjs CHANGED
@@ -1,33 +1,33 @@
1
- #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- import { createInterface } from "node:readline";
4
- import { configuredProvider, loadConfig } from "./config.mjs";
5
- import { routeRequest } from "./route.mjs";
6
-
7
- const args = parseArgs(process.argv.slice(2));
8
- const { config } = await loadConfig();
9
- const input = args.input ? await readFile(args.input, "utf8") : null;
10
- const lines = input === null ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : [input];
11
-
12
- for await (const line of lines) {
13
- if (!line.trim()) continue;
14
- try {
15
- const request = JSON.parse(line);
16
- const { provider = args.provider ?? configuredProvider(config), engine = args.engine ?? "native", ...payload } = request;
17
- const decision = await routeRequest(payload, { provider, engine });
18
- process.stdout.write(`${JSON.stringify(decision)}\n`);
19
- } catch (error) {
20
- process.stdout.write(`${JSON.stringify({ status: "error", reason: error instanceof Error ? error.message : String(error), execution: { enabled: false, status: "not_started" } })}\n`);
21
- }
22
- }
23
-
24
- function parseArgs(argv) {
25
- const result = {};
26
- for (let index = 0; index < argv.length; index += 1) {
27
- const value = argv[index];
28
- if (value === "--input") result.input = argv[++index];
29
- else if (value === "--provider") result.provider = argv[++index];
30
- else if (value === "--engine") result.engine = argv[++index];
31
- }
32
- return result;
33
- }
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { createInterface } from "node:readline";
4
+ import { configuredProvider, loadConfig } from "./config.mjs";
5
+ import { routeRequest } from "./route.mjs";
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const { config } = await loadConfig();
9
+ const input = args.input ? await readFile(args.input, "utf8") : null;
10
+ const lines = input === null ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : [input];
11
+
12
+ for await (const line of lines) {
13
+ if (!line.trim()) continue;
14
+ try {
15
+ const request = JSON.parse(line);
16
+ const { provider = args.provider ?? configuredProvider(config), engine = args.engine ?? "native", ...payload } = request;
17
+ const decision = await routeRequest(payload, { provider, engine, config });
18
+ process.stdout.write(`${JSON.stringify(decision)}\n`);
19
+ } catch (error) {
20
+ process.stdout.write(`${JSON.stringify({ status: "error", reason: error instanceof Error ? error.message : String(error), execution: { enabled: false, status: "not_started" } })}\n`);
21
+ }
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const result = {};
26
+ for (let index = 0; index < argv.length; index += 1) {
27
+ const value = argv[index];
28
+ if (value === "--input") result.input = argv[++index];
29
+ else if (value === "--provider") result.provider = argv[++index];
30
+ else if (value === "--engine") result.engine = argv[++index];
31
+ }
32
+ return result;
33
+ }
@@ -0,0 +1,196 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { decisionEnvelope } from "./contract.mjs";
3
+ import { imageDimensions, normalizeImageAssets } from "./images.mjs";
4
+ import { resolveProvider } from "./providers/index.mjs";
5
+ import { routeRequest } from "./route.mjs";
6
+
7
+ const OPERATIONS = new Set(["click", "type", "key", "scroll", "handoff"]);
8
+ const CONSEQUENTIAL = new Set(["click", "type", "key"]);
9
+ const ARGUMENTS = {
10
+ click: ["x", "y", "button"],
11
+ type: ["text"],
12
+ key: ["keys"],
13
+ scroll: ["direction", "amount", "x", "y"],
14
+ handoff: ["reason"],
15
+ };
16
+
17
+ export async function decideComputerStep(input, options = {}) {
18
+ const enabled = options.enabled === true || options.enabled !== false && process.env.JEV_COMPUTER_USE === "1";
19
+ if (!enabled) return failed(null, null, "computer_use_disabled", "Computer-use advice is disabled", options);
20
+
21
+ let request;
22
+ let actions;
23
+ let screenshot;
24
+ let thresholds;
25
+ try {
26
+ if (!object(input) || !text(input.goal, 8_000)) throw new TypeError("goal is required");
27
+ const supplied = input.observation?.screenshot;
28
+ if (!object(supplied)) throw new TypeError("screenshot is required");
29
+ const reference = randomUUID();
30
+ const assets = normalizeImageAssets({ [reference]: { type: "image", format: supplied.format, data_base64: supplied.data_base64 } });
31
+ const dimensions = imageDimensions(assets[reference]);
32
+ if (supplied.width !== dimensions.width || supplied.height !== dimensions.height) throw new TypeError("screenshot dimensions do not match image");
33
+ screenshot = {
34
+ ...dimensions,
35
+ format: assets[reference].format,
36
+ sha256: `sha256:${createHash("sha256").update(Buffer.from(assets[reference].data_base64, "base64")).digest("hex")}`,
37
+ requires_current: true,
38
+ };
39
+ if (!Array.isArray(input.actions) || input.actions.length > 32) throw new TypeError("actions must contain at most 32 candidates");
40
+ actions = input.actions.map((candidate) => normalizeAction(candidate, dimensions));
41
+ if (new Set(actions.map((candidate) => candidate.id)).size !== actions.length) throw new TypeError("action ids must be unique");
42
+ if (input.policy !== undefined && !object(input.policy) || options.policy !== undefined && !object(options.policy)) throw new TypeError("invalid policy");
43
+ const policy = { ...(input.policy ?? {}), ...(options.policy ?? {}) };
44
+ thresholds = {
45
+ visibility: policy.min_target_visibility ?? 0.9,
46
+ box: policy.min_box_confidence ?? 0.8,
47
+ };
48
+ if (!probability(thresholds.visibility) || thresholds.visibility === 0 || !probability(thresholds.box) || thresholds.box === 0) throw new TypeError("invalid grounding thresholds");
49
+ const observation = { screenshot };
50
+ for (const key of ["visible_text", "application"]) {
51
+ if (input.observation[key] !== undefined) {
52
+ if (!text(input.observation[key], key === "visible_text" ? 8_000 : 240, true)) throw new TypeError(`invalid ${key}`);
53
+ observation[key] = input.observation[key];
54
+ }
55
+ }
56
+ if (input.actor_permissions !== undefined && (!Array.isArray(input.actor_permissions) || input.actor_permissions.length > 32 || input.actor_permissions.some((permission) => !text(permission, 240)))) throw new TypeError("invalid actor_permissions");
57
+ request = {
58
+ schema_version: 1,
59
+ harness: text(input.harness, 240) ? input.harness : "computer",
60
+ intent: `Computer goal: ${input.goal}`,
61
+ context: {
62
+ computer: {
63
+ observation,
64
+ actions: actions.map(({ id, operation, args, target }) => ({ id, operation, args, ...(target ? { target } : {}) })),
65
+ },
66
+ },
67
+ assets,
68
+ actor_permissions: input.actor_permissions ?? [],
69
+ capabilities: actions.map((candidate) => ({
70
+ ...candidate,
71
+ kind: "tool",
72
+ name: candidate.name ?? `${candidate.operation}: ${candidate.id}`,
73
+ description: candidate.description ?? `${candidate.operation} ${candidate.target ?? JSON.stringify(candidate.args)}`,
74
+ permissions: [...new Set(["computer", ...(candidate.permissions ?? [])])],
75
+ risk: candidate.risk ?? (CONSEQUENTIAL.has(candidate.operation) ? "medium" : "low"),
76
+ policy: { ...candidate.policy, requires_confirmation: CONSEQUENTIAL.has(candidate.operation) || candidate.consequential === true || candidate.requires_confirmation === true || candidate.policy?.requires_confirmation === true },
77
+ execution: { mode: "host", target: candidate.id },
78
+ })),
79
+ policy,
80
+ };
81
+ } catch {
82
+ return failed(null, null, "invalid_input", "Computer advice requires valid bounded actions and a matching PNG/JPEG screenshot", options);
83
+ }
84
+
85
+ let provider;
86
+ try {
87
+ provider = resolveProvider(options.provider ?? "typesafe", options);
88
+ } catch {
89
+ return failed(request, null, "provider_error", "Computer advice provider is unavailable", options);
90
+ }
91
+ if (provider.supportsImages !== true) return failed(request, null, "unsupported_images", "Provider does not support image input", options);
92
+
93
+ let decision;
94
+ try {
95
+ decision = await routeRequest(request, { ...options, engine: "native", discovery: undefined, provider });
96
+ } catch {
97
+ return failed(request, null, "invalid_input", "Computer routing request could not be validated", options);
98
+ }
99
+ if (!["selected", "needs_confirmation"].includes(decision.status)) return { request, decision, action: null };
100
+ const selected = actions.find((candidate) => candidate.id === decision.selected);
101
+ if (!selected) return failed(request, decision, "invalid_selection", "Selected action is outside the host candidate set", options);
102
+ let action = { ...selected, screenshot };
103
+ if (selected.operation === "click" && selected.args.x === undefined) {
104
+ if (typeof provider.evaluate !== "function") return failed(request, decision, "grounding_unavailable", "Provider cannot ground the selected target", options);
105
+ let raw;
106
+ try {
107
+ raw = await provider.evaluate({
108
+ state: { screenshot: Object.keys(request.assets)[0], target: selected.target },
109
+ assets: request.assets,
110
+ questions: {
111
+ target_visible: { type: "noul", instructions: "Is the exact target described in `target` clearly visible and uniquely identifiable in `screenshot`?" },
112
+ target_box: { type: "box", instructions: "Find the exact target described in `target` in `screenshot`." },
113
+ },
114
+ });
115
+ } catch {
116
+ return failed(request, decision, "provider_error", "Selected target grounding failed", options);
117
+ }
118
+ const visible = raw?.answers?.target_visible;
119
+ const located = raw?.answers?.target_box;
120
+ const box = located?.box;
121
+ if (visible?.type !== "noul" || !probability(visible.noul) || located?.type !== "box" || !probability(located.confidence) || !object(box)
122
+ || ![box.left, box.right, box.top, box.bottom].every(probability) || box.left >= box.right || box.top >= box.bottom) {
123
+ return failed(request, decision, "malformed_grounding", "Provider returned invalid target visibility or box", options);
124
+ }
125
+ if (visible.noul < thresholds.visibility) return failed(request, decision, "target_not_visible", "Selected target is not confidently visible", options);
126
+ if (located.confidence < thresholds.box) return failed(request, decision, "low_grounding_confidence", "Selected target box is below the host confidence threshold", options);
127
+ action = {
128
+ ...action,
129
+ args: {
130
+ ...selected.args,
131
+ x: Math.min(screenshot.width - 1, Math.floor((box.left + box.right) / 2 * screenshot.width)),
132
+ y: Math.min(screenshot.height - 1, Math.floor((box.top + box.bottom) / 2 * screenshot.height)),
133
+ },
134
+ grounding: {
135
+ box: { left: box.left, right: box.right, top: box.top, bottom: box.bottom },
136
+ visibility: visible.noul,
137
+ confidence: located.confidence,
138
+ coordinate_space: "screenshot_pixels",
139
+ },
140
+ };
141
+ }
142
+ return { request, decision, action };
143
+ }
144
+
145
+ function normalizeAction(raw, dimensions) {
146
+ if (!object(raw) || !text(raw.id, 240) || raw.id !== raw.id.trim() || !OPERATIONS.has(raw.operation)) throw new TypeError("invalid action");
147
+ const args = raw.args ?? {};
148
+ if (!object(args) || Object.keys(args).some((key) => !ARGUMENTS[raw.operation].includes(key))) throw new TypeError("invalid action arguments");
149
+ const hasCoordinates = args.x !== undefined || args.y !== undefined;
150
+ if (hasCoordinates && (!Number.isInteger(args.x) || args.x < 0 || args.x >= dimensions.width || !Number.isInteger(args.y) || args.y < 0 || args.y >= dimensions.height)) throw new TypeError("coordinates must be inside screenshot");
151
+ if (raw.target !== undefined && !text(raw.target, 1_000)) throw new TypeError("invalid target description");
152
+ if (raw.operation === "click" && (!hasCoordinates && !raw.target || args.button !== undefined && !["left", "middle", "right"].includes(args.button))) throw new TypeError("invalid click");
153
+ if (raw.operation === "type" && !text(args.text, 8_000)) throw new TypeError("invalid text");
154
+ if (raw.operation === "key" && (!Array.isArray(args.keys) || args.keys.length < 1 || args.keys.length > 8 || args.keys.some((key) => !text(key, 80)))) throw new TypeError("invalid keys");
155
+ if (raw.operation === "scroll" && (!["up", "down", "left", "right"].includes(args.direction) || args.amount !== undefined && (!Number.isInteger(args.amount) || args.amount < 1 || args.amount > 10_000))) throw new TypeError("invalid scroll");
156
+ if (raw.operation === "handoff" && args.reason !== undefined && !text(args.reason, 1_000)) throw new TypeError("invalid handoff reason");
157
+ if (raw.permissions !== undefined && (!Array.isArray(raw.permissions) || raw.permissions.some((permission) => !text(permission, 240)))) throw new TypeError("invalid permissions");
158
+ for (const key of ["name", "description"]) {
159
+ if (raw[key] !== undefined && !text(raw[key], key === "name" ? 240 : 2_000)) throw new TypeError(`invalid ${key}`);
160
+ }
161
+ for (const key of ["available", "requires_confirmation", "consequential", "verified"]) {
162
+ if (raw[key] !== undefined && typeof raw[key] !== "boolean") throw new TypeError(`invalid ${key}`);
163
+ }
164
+ if (raw.policy !== undefined && (!object(raw.policy) || raw.policy.requires_confirmation !== undefined && typeof raw.policy.requires_confirmation !== "boolean")) throw new TypeError("invalid confirmation policy");
165
+ if (raw.availability !== undefined && (!object(raw.availability) || raw.availability.available !== undefined && typeof raw.availability.available !== "boolean")) throw new TypeError("invalid availability");
166
+ const action = { id: raw.id, operation: raw.operation, args: structuredClone(args) };
167
+ for (const key of ["target", "name", "description", "permissions", "risk", "available", "availability", "requires_confirmation", "policy", "consequential", "source", "verified", "metadata"]) {
168
+ if (raw[key] !== undefined) action[key] = structuredClone(raw[key]);
169
+ }
170
+ return action;
171
+ }
172
+
173
+ function failed(request, decision, type, reason, options) {
174
+ const fallback = { type, reason };
175
+ return {
176
+ request,
177
+ action: null,
178
+ decision: decision ? { ...decision, status: "fallback", reason, fallback } : decisionEnvelope({
179
+ status: "fallback", reason, fallback,
180
+ provider: typeof options.provider === "string" ? options.provider : options.provider?.name ?? "typesafe",
181
+ latencyMs: 0, candidateCount: request?.capabilities.length ?? 0, contextBytes: 0,
182
+ }),
183
+ };
184
+ }
185
+
186
+ function object(value) {
187
+ return value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
188
+ }
189
+
190
+ function text(value, maximum, allowEmpty = false) {
191
+ return typeof value === "string" && value.length <= maximum && (allowEmpty || value.trim().length > 0);
192
+ }
193
+
194
+ function probability(value) {
195
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
196
+ }
package/src/config.mjs CHANGED
@@ -1,75 +1,86 @@
1
- import { access, readFile } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
-
4
- export const DEFAULT_CONFIG = Object.freeze({
5
- schema_version: 1,
6
- provider: "demo",
7
- replay_cases: ".jev/replay/cases.jsonl",
8
- features: {
9
- browser_fast_path: false,
10
- supervision: false,
11
- context_filter: null,
12
- },
13
- providers: {
14
- openrouter: {
15
- api_key_env: "OPENROUTER_API_KEY",
16
- endpoint_env: "OPENROUTER_DECISIONS_ENDPOINT",
17
- model_env: "OPENROUTER_DECISIONS_MODEL",
18
- },
19
- typesafe: {
20
- api_key_env: "TYPESAFE_API_KEY",
21
- endpoint_env: "TYPESAFE_ENDPOINT",
22
- model_env: "TYPESAFE_MODEL",
23
- },
24
- },
25
- });
26
-
27
- export async function loadConfig({ cwd = process.cwd(), path = process.env.JEV_CONFIG } = {}) {
28
- const configPath = resolve(path || join(cwd, ".jev", "config.json"));
29
- try {
30
- const parsed = JSON.parse(await readFile(configPath, "utf8"));
31
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32
- return { config: { ...DEFAULT_CONFIG }, path: configPath, error: "config must be a JSON object" };
33
- }
34
- return {
35
- config: mergeConfig(parsed),
36
- path: configPath,
37
- error: null,
38
- };
39
- } catch (error) {
40
- if (error?.code === "ENOENT") return { config: { ...DEFAULT_CONFIG }, path: configPath, error: null };
41
- return { config: { ...DEFAULT_CONFIG }, path: configPath, error: error instanceof Error ? error.message : String(error) };
42
- }
43
- }
44
-
45
- export function configuredProvider(config, explicitProvider) {
46
- return explicitProvider ?? process.env.JEV_LAYER_PROVIDER ?? config?.provider ?? DEFAULT_CONFIG.provider;
47
- }
48
-
49
- export function configuredReplayPath(config) {
50
- return process.env.JEV_REPLAY_CASES ?? config?.replay_cases ?? DEFAULT_CONFIG.replay_cases;
51
- }
52
-
53
- export async function pathExists(path) {
54
- try {
55
- await access(path);
56
- return true;
57
- } catch {
58
- return false;
59
- }
60
- }
61
-
62
- function mergeConfig(parsed) {
63
- return {
64
- ...DEFAULT_CONFIG,
65
- ...parsed,
66
- features: {
67
- ...DEFAULT_CONFIG.features,
68
- ...(parsed.features && typeof parsed.features === "object" ? parsed.features : {}),
69
- },
70
- providers: {
71
- ...DEFAULT_CONFIG.providers,
72
- ...(parsed.providers && typeof parsed.providers === "object" ? parsed.providers : {}),
73
- },
74
- };
75
- }
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export const DEFAULT_CONFIG = Object.freeze({
5
+ schema_version: 1,
6
+ provider: "demo",
7
+ replay_cases: ".jev/replay/cases.jsonl",
8
+ features: {
9
+ browser_fast_path: false,
10
+ computer_use: false,
11
+ supervision: false,
12
+ context_filter: null,
13
+ },
14
+ providers: {
15
+ openrouter: {
16
+ api_key_env: "OPENROUTER_API_KEY",
17
+ endpoint_env: "OPENROUTER_DECISIONS_ENDPOINT",
18
+ model_env: "OPENROUTER_DECISIONS_MODEL",
19
+ },
20
+ typesafe: {
21
+ api_key_env: "TYPESAFE_API_KEY",
22
+ endpoint_env: "TYPESAFE_ENDPOINT",
23
+ model_env: "TYPESAFE_MODEL",
24
+ },
25
+ },
26
+ });
27
+
28
+ export async function loadConfig({ cwd = process.cwd(), path = process.env.JEV_CONFIG } = {}) {
29
+ const configPath = resolve(path || join(cwd, ".jev", "config.json"));
30
+ try {
31
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
33
+ return { config: { ...DEFAULT_CONFIG }, path: configPath, error: "config must be a JSON object" };
34
+ }
35
+ return {
36
+ config: mergeConfig(parsed),
37
+ path: configPath,
38
+ error: null,
39
+ };
40
+ } catch (error) {
41
+ if (error?.code === "ENOENT") return { config: { ...DEFAULT_CONFIG }, path: configPath, error: null };
42
+ return { config: { ...DEFAULT_CONFIG }, path: configPath, error: error instanceof Error ? error.message : String(error) };
43
+ }
44
+ }
45
+
46
+ export function configuredProvider(config, explicitProvider) {
47
+ return explicitProvider ?? process.env.JEV_LAYER_PROVIDER ?? config?.provider ?? DEFAULT_CONFIG.provider;
48
+ }
49
+
50
+ export function configuredProviderOptions(config, provider) {
51
+ if (!config?.providers?.[provider]) return {};
52
+ const names = { ...DEFAULT_CONFIG.providers[provider], ...config.providers[provider] };
53
+ return {
54
+ apiKey: process.env[names.api_key_env] ?? "",
55
+ endpoint: process.env[names.endpoint_env],
56
+ model: process.env[names.model_env],
57
+ };
58
+ }
59
+
60
+ export function configuredReplayPath(config) {
61
+ return process.env.JEV_REPLAY_CASES ?? config?.replay_cases ?? DEFAULT_CONFIG.replay_cases;
62
+ }
63
+
64
+ export async function pathExists(path) {
65
+ try {
66
+ await access(path);
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function mergeConfig(parsed) {
74
+ return {
75
+ ...DEFAULT_CONFIG,
76
+ ...parsed,
77
+ features: {
78
+ ...DEFAULT_CONFIG.features,
79
+ ...(parsed.features && typeof parsed.features === "object" ? parsed.features : {}),
80
+ },
81
+ providers: {
82
+ ...DEFAULT_CONFIG.providers,
83
+ ...(parsed.providers && typeof parsed.providers === "object" ? parsed.providers : {}),
84
+ },
85
+ };
86
+ }
@@ -1,56 +1,56 @@
1
- import { byteLength, stableJson } from "./contract.mjs";
2
- import { filterContext, normalizeRelevanceMode } from "./relevance-filter.mjs";
3
- const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|cookie|credential|private[_-]?key|session)/i;
4
- const MAX_STRING = 2_000;
5
- const MAX_DEPTH = 4;
6
-
7
- export function sanitizeContext(value, depth = 0, key = "") {
8
- if (SECRET_KEY.test(key)) return "[REDACTED]";
9
- if (depth > MAX_DEPTH) return "[DEPTH_LIMIT]";
10
- if (typeof value === "string") return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
11
- if (value === null || typeof value === "number" || typeof value === "boolean") return value;
12
- if (Array.isArray(value)) return value.slice(0, 32).map((item) => sanitizeContext(item, depth + 1, key));
13
- if (value && typeof value === "object") {
14
- return Object.fromEntries(Object.entries(value).slice(0, 64).map(([childKey, childValue]) => [childKey, sanitizeContext(childValue, depth + 1, childKey)]));
15
- }
16
- return String(value);
17
- }
18
-
19
- export function projectState(request, capabilities, maxBytes = 6_000, options = {}) {
20
- const configuredMode = options.mode ?? request.policy?.context_filter_mode ?? process.env.JEV_CONTEXT_FILTER;
21
- const relevanceMode = normalizeRelevanceMode(configuredMode);
22
- const relevance = filterContext(request.context, { mode: configuredMode, recent: options.recent });
23
- const state = {
24
- intent: request.intent.length > MAX_STRING ? `${request.intent.slice(0, MAX_STRING)}…` : request.intent,
25
- context: sanitizeContext(relevance.context),
26
- capabilities: capabilities.map((capability) => ({
27
- id: capability.id,
28
- kind: capability.kind,
29
- name: capability.name,
30
- description: capability.description,
31
- risk: capability.risk,
32
- })),
33
- };
34
- if (relevanceMode) state.context_filter = relevance.report;
35
-
36
- let encoded = stableJson(state);
37
- if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: false };
38
- if (relevanceMode) {
39
- state.context_filter = { ...state.context_filter, over_budget: true };
40
- return { state, context_bytes: byteLength(encoded), truncated: true };
41
- }
42
-
43
- state.context = {};
44
- state.context_truncated = true;
45
- encoded = stableJson(state);
46
- if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
47
-
48
- state.capabilities = state.capabilities.map(({ id, kind, name, risk }) => ({ id, kind, name, risk }));
49
- encoded = stableJson(state);
50
- if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
51
-
52
- state.capabilities = state.capabilities.slice(0, 32);
53
- state.capabilities_truncated = true;
54
- state.intent = state.intent.slice(0, Math.max(128, maxBytes - byteLength(stableJson(state))));
55
- return { state, context_bytes: byteLength(stableJson(state)), truncated: true };
56
- }
1
+ import { byteLength, stableJson } from "./contract.mjs";
2
+ import { filterContext, normalizeRelevanceMode } from "./relevance-filter.mjs";
3
+ const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|cookie|credential|private[_-]?key|session|data_base64)/i;
4
+ const MAX_STRING = 2_000;
5
+ const MAX_DEPTH = 4;
6
+
7
+ export function sanitizeContext(value, depth = 0, key = "") {
8
+ if (SECRET_KEY.test(key)) return "[REDACTED]";
9
+ if (depth > MAX_DEPTH) return "[DEPTH_LIMIT]";
10
+ if (typeof value === "string") return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
11
+ if (value === null || typeof value === "number" || typeof value === "boolean") return value;
12
+ if (Array.isArray(value)) return value.slice(0, 32).map((item) => sanitizeContext(item, depth + 1, key));
13
+ if (value && typeof value === "object") {
14
+ return Object.fromEntries(Object.entries(value).slice(0, 64).map(([childKey, childValue]) => [childKey, sanitizeContext(childValue, depth + 1, childKey)]));
15
+ }
16
+ return String(value);
17
+ }
18
+
19
+ export function projectState(request, capabilities, maxBytes = 6_000, options = {}) {
20
+ const configuredMode = options.mode ?? request.policy?.context_filter_mode ?? process.env.JEV_CONTEXT_FILTER;
21
+ const relevanceMode = normalizeRelevanceMode(configuredMode);
22
+ const relevance = filterContext(request.context, { mode: configuredMode, recent: options.recent });
23
+ const state = {
24
+ intent: request.intent.length > MAX_STRING ? `${request.intent.slice(0, MAX_STRING)}…` : request.intent,
25
+ context: sanitizeContext(relevance.context),
26
+ capabilities: capabilities.map((capability) => ({
27
+ id: capability.id,
28
+ kind: capability.kind,
29
+ name: capability.name,
30
+ description: capability.description,
31
+ risk: capability.risk,
32
+ })),
33
+ };
34
+ if (relevanceMode) state.context_filter = relevance.report;
35
+
36
+ let encoded = stableJson(state);
37
+ if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: false };
38
+ if (relevanceMode) {
39
+ state.context_filter = { ...state.context_filter, over_budget: true };
40
+ return { state, context_bytes: byteLength(encoded), truncated: true };
41
+ }
42
+
43
+ state.context = {};
44
+ state.context_truncated = true;
45
+ encoded = stableJson(state);
46
+ if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
47
+
48
+ state.capabilities = state.capabilities.map(({ id, kind, name, risk }) => ({ id, kind, name, risk }));
49
+ encoded = stableJson(state);
50
+ if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
51
+
52
+ state.capabilities = state.capabilities.slice(0, 32);
53
+ state.capabilities_truncated = true;
54
+ state.intent = state.intent.slice(0, Math.max(128, maxBytes - byteLength(stableJson(state))));
55
+ return { state, context_bytes: byteLength(stableJson(state)), truncated: true };
56
+ }