diff --git a/Cargo.lock b/Cargo.lock index 853c288396..307e61334e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4836,6 +4836,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "maka-jev" +version = "0.2.0" +dependencies = [ + "futures-util", + "maka-plugins", + "maka-runtime", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "url", + "uuid", +] + [[package]] name = "maka-js-runtime" version = "0.2.0" @@ -5068,6 +5085,7 @@ dependencies = [ "maka-fs-tools", "maka-graph", "maka-insights", + "maka-jev", "maka-js-runtime", "maka-model", "maka-network", diff --git a/Cargo.toml b/Cargo.toml index 4b78681830..230cc8a5a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ [workspace] default-members = ["crates/cli"] exclude = ["native/gitoxide-helper", "native/runtime-host-peer", "native/runtime-host-windows-task-launcher", "experiments/windows-sandbox/launcher"] -members = ["crates/session-import", "crates/insights", "crates/sandbox", "crates/web", "crates/workhub", "crates/assistant", "crates/scheduler", "crates/graph", "crates/plugins", "crates/skills", "crates/apply-patch", "crates/cli", "crates/runtime", "crates/event-log", "crates/js-runtime", "crates/runtime-host", "crates/protocol", "crates/model", "crates/responses", "crates/providers", "crates/transport", "crates/agent", "crates/config", "crates/presentation", "crates/tools", "crates/tool-catalog", "crates/fs-tools", "crates/process", "crates/client-capability", "crates/network"] +members = ["crates/jev", "crates/session-import", "crates/insights", "crates/sandbox", "crates/web", "crates/workhub", "crates/assistant", "crates/scheduler", "crates/graph", "crates/plugins", "crates/skills", "crates/apply-patch", "crates/cli", "crates/runtime", "crates/event-log", "crates/js-runtime", "crates/runtime-host", "crates/protocol", "crates/model", "crates/responses", "crates/providers", "crates/transport", "crates/agent", "crates/config", "crates/presentation", "crates/tools", "crates/tool-catalog", "crates/fs-tools", "crates/process", "crates/client-capability", "crates/network"] resolver = "3" [workspace.package] @@ -44,6 +44,7 @@ maka-tui = { path = "crates/tui" } ratatui = { version = "0.30.2", default-features = false, features = ["crossterm"] } crossterm = { version = "0.29.0", features = ["event-stream"] } maka-sandbox = { path = "crates/sandbox" } +maka-jev = { path = "crates/jev" } maka-web = { path = "crates/web" } maka-assistant = { path = "crates/assistant" } maka-scheduler = { path = "crates/scheduler" } diff --git a/crates/cli/DEPENDENCIES.rust.tsv b/crates/cli/DEPENDENCIES.rust.tsv index 2139e5a4d0..2195d74de4 100644 --- a/crates/cli/DEPENDENCIES.rust.tsv +++ b/crates/cli/DEPENDENCIES.rust.tsv @@ -392,6 +392,7 @@ maka-event-log@0.2.0 X maka-fs-tools@0.2.0 X maka-graph@0.2.0 X maka-insights@0.2.0 X +maka-jev@0.2.0 X maka-js-runtime@0.2.0 X maka-model@0.2.0 X maka-network@0.2.0 X @@ -405,6 +406,7 @@ maka-runtime@0.2.0 X maka-runtime-host@0.2.0 X maka-sandbox@0.2.0 X maka-scheduler@0.2.0 X +maka-session-import@0.2.0 X maka-skills@0.2.0 X maka-tool-catalog@0.2.0 X maka-tools@0.2.0 X diff --git a/crates/jev/Cargo.toml b/crates/jev/Cargo.toml new file mode 100644 index 0000000000..14eed093da --- /dev/null +++ b/crates/jev/Cargo.toml @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "maka-jev" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +maka-plugins.workspace = true +maka-runtime.workspace = true +futures-util.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true +uuid.workspace = true +sha2.workspace = true +url = "2.5.8" diff --git a/crates/jev/README.md b/crates/jev/README.md new file mode 100644 index 0000000000..4178469bff --- /dev/null +++ b/crates/jev/README.md @@ -0,0 +1,90 @@ + + +# Jev structured evaluation + +`maka.jev` is a statically linked Profile plugin. It publishes the callable +service `maka.jev.evaluate`; the separate Desktop UI entry contributes a Settings +page. Consumers use the same service from native Rust or JavaScript. + +## Configuration + +The Settings page manages an explicit enable switch, a full HTTP(S) endpoint, +model, and timeout (100–60000 ms). The defaults are disabled, +`https://api.typesafe.ai/v1/systemone`, `jev-latest`, and 8000 ms. +Any compatible endpoint can replace the default, including a local gateway. + +API keys and **all custom header values** are stored in Host credentials. Reads +return only whether credentials exist, their revision, and header names. Saving +credentials replaces the endpoint's complete authentication/header record. An +empty record explicitly permits an unauthenticated local service. A supplied API +key generates `Authorization: Bearer …`; leave it empty to supply another +Authorization scheme through custom headers. Content-Type is always JSON. +Routing/framing headers and case-insensitive duplicates are rejected. + +Credentials are bound to the full endpoint URL. Changing a URL does not forward +previous credentials; returning to a previously configured URL restores that +endpoint's credentials. Configuration and credentials use revision checks so a +stale Settings page cannot overwrite another writer. Incognito mode blocks calls. + +## Consumer contract + +The request and answer shapes follow [TypeSafe's System One API](https://docs.typesafe.ai/api). +Callers provide `state` and named `questions`; the plugin supplies the configured +`model`. `noul`, `choice`, and `score` answers retain probability/uncertainty and +provider token usage. Limits: 64 questions, 32 KiB evaluation input, 64 KiB response. + +A consumer declares `inject: ["maka.jev.evaluate"]` on its Composition Entry +(or Definition), so activation waits for the provider. Settings are shared at +Profile scope; service binding/intercept configuration does not override them. + +Native consumers resolve `decision::SERVICE` through their injected Services view +and call with `decision::Evaluation`, receiving `decision::EvaluationResult`. +JSON consumers use the same method name and JSON values. For example: + +```json +{ + "state": { "tests": "passed", "remaining": [] }, + "questions": { + "complete": { "type": "noul", "instructions": "Is all requested work complete?" } + } +} +``` + +Every call requires an admitted caller scope with Host network authority. Service +availability does not grant authority. Desktop's test button obtains a separate +Network authorization and tests the saved configuration. Plugin retirement +invalidates captured service handles. Cancellation and timeout settle the scoped +HTTP resources before returning. No redirects or automatic retries forward +credentials or duplicate a potentially paid operation; a timeout/network error +can mean the provider processed the request. Provider response bodies are never +included in errors. + +Jev is a structured evaluation service, not a chat model adapter. Returned usage +is provider-reported metadata; it is not added to Host model usage accounting. +A consumer must handle unavailable service, missing configuration, authorization +refusal, uncertain network outcomes, and probabilistic answers explicitly. + +## Verification + +`cargo test -p maka-jev` covers protocol validation, native/JSON consumers, +retirement, custom headers, privacy, redirects, limits, timeout and cancellation. +`cargo test -p maka-runtime-host --test integration jev_plugin` covers the actual +composition/Remote configuration path, revision conflicts, secret non-disclosure, +endpoint isolation, retirement and Host restart. Tests use no paid API key. diff --git a/crates/jev/src/client.tsx b/crates/jev/src/client.tsx new file mode 100644 index 0000000000..9aa687970a --- /dev/null +++ b/crates/jev/src/client.tsx @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEffect, useRef, useState } from 'react'; +import type { ClientContext, ClientPlugin, ClientSlots } from '@maka-agent/plugin-sdk/client'; +type Settings = { enabled: boolean; url: string; model: string; timeoutMs: number }; +type Snapshot = { + revision: number | null; + settings: Settings; + credentialRevision: number | null; + configured: boolean; + headerNames: string[]; +}; +type Request = + | { kind: 'read' } + | { kind: 'configure'; expectedRevision: number | null; settings: Settings } + | { + kind: 'credential'; + url: string; + expectedRevision: number | null; + secret: { apiKey: string | null; headers: Record } | null; + } + | { kind: 'test'; operationId: string }; +type Response = + | { kind: 'snapshot'; snapshot: Snapshot } + | { kind: 'tested'; answer: import('@maka-agent/plugin-sdk/host').Json }; +function SettingsPage({ + context, + locale, +}: ClientSlots['settings.page'] & { context: ClientContext }) { + const zh = locale !== 'en'; + const [snapshot, setSnapshot] = useState(null); + const [draft, setDraft] = useState(null); + const [secret, setSecret] = useState(''); + const [headers, setHeaders] = useState('{}'); + const [busy, setBusy] = useState(false); + const [status, setStatus] = useState(''); + const [error, setError] = useState(''); + const version = useRef(0); + const active = useRef(false); + const call = context.remote.method('request'); + useEffect(() => { + const generation = ++version.current; + active.current = false; + setBusy(false); + setSnapshot(null); + setDraft(null); + setSecret(''); + setHeaders('{}'); + setStatus(''); + setError(''); + void call({ kind: 'read' }) + .then((r) => { + if (generation === version.current && !context.signal.aborted && r.kind === 'snapshot') { + setSnapshot(r.snapshot); + setDraft(r.snapshot.settings); + } + }) + .catch((e) => { + if (generation === version.current) setError(String(e)); + }); + return () => { + version.current++; + }; + }, [context]); + async function run(request: Request) { + if (active.current) return; + active.current = true; + setBusy(true); + setError(''); + setStatus(''); + const generation = version.current; + try { + const result = await call(request); + if (generation !== version.current || context.signal.aborted) return; + if (result.kind === 'snapshot') { + setSnapshot(result.snapshot); + setDraft(result.snapshot.settings); + setSecret(''); + setHeaders('{}'); + } else setStatus(zh ? '连接测试成功' : 'Connection test succeeded'); + } catch (e) { + if (generation === version.current && !context.signal.aborted) setError(String(e)); + } finally { + if (generation === version.current) { + active.current = false; + setBusy(false); + } + } + } + const changed = draft && snapshot && JSON.stringify(draft) !== JSON.stringify(snapshot.settings); + return ( +
+

+ {zh + ? '为插件提供 Jev 结构化决策。密钥分别保存到每个端点;更改 URL 后请为新端点配置密钥。隐私模式下禁止调用。' + : 'Structured Jev decisions for plugins. Keys are saved per endpoint; configure a key after changing the URL. Calls are disabled in incognito mode.'} +

+ {error &&

{error}

} + {status &&

{status}

} + + {snapshot && draft && ( +
+ + + + + + +