feat(typesafe): support local System One decision models (#154059)

* fix(typesafe): distribute as an official external plugin

Exclude TypeSafe runtime from the core package and enroll @openclaw/typesafe in npm and ClawHub publication. Preserve the plugin ID, protected credentials, and decision-model configuration. Require the post-2026.9.5 decision API and document the pending supporting release.

* fix(typesafe): register the official external install catalog

Keep official package discovery and trust aligned with the TypeSafe distribution cutover. Existing catalog completeness and ClawHub-counterpart checks pass.

* feat(typesafe): support local System One decision models

Add an explicit loopback endpoint for Kev, without forwarding hosted credentials or changing managed proxy authority. Preserve strict answer validation while adapting local instructions, Score legends, and timing metadata. Keep the declared local model out of hosted inference. Document endpoint scope and runtime ownership.

* fix(typesafe): validate local payload types and document Kev

Preserve discriminated question types without assertions and regenerate config documentation for the requested baseUrl field. Add the pinned Kev setup and API-test commands exercised in live inference. Core and channel config budgets remain unchanged; the plugin budget grows by one intentional field.

* docs(agents): allow exact synthetic scanner fixture qualification
This commit is contained in:
Peter Steinberger 2026-09-20 19:44:29 -07:00 • committed by GitHub
parent 894fb140a2
commit 5acfb83d9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 727 additions and 68 deletions

View file

@ -112,7 +112,7 @@ not authorize local execution or a broader test plan.
- Bug fixes within the authorized task do not need renewed approval, including compatible SDK changes needed to restore intended behavior. Ask again for new configuration options, breaking public contracts, intentional changes to schemas, durability, retention, or permissions beyond the bug fix, paid services, or destructive actions. Preserve FIFO ordering, live-authority and integrity checks, and settlement of write-capable work.
- Protocol/version bumps, dependency patches/overrides/vendor changes, paid services, releases, and publishing need explicit approval; fix/ship authority does not imply release authority. Advisory workflows require an explicit request for that security action.
- Extended-stable is one line: the trailing completed month relative to `main`'s version. Older `.33+` lines retire when `main` advances another month; publishing a retired line needs an explicit maintainer decision, not a routine guard bypass.
- Baseline, snapshot, ignore, and expected-failure exceptions need approval; exact shrink-only ratchet updates are maintenance.
- Baseline, snapshot, ignore, and expected-failure exceptions need approval, except narrow scanner qualifications for verified synthetic test fixtures. Agents may add these within the authorized task without asking when matching is bound to exact fixture bytes and source location, scanning and verification stay enabled, and proof rejects changed inputs. Broad suppressions and uncertain or real credentials still require approval. Exact shrink-only ratchet updates are maintenance.
- `CODEOWNERS` routes review; check live GitHub enforcement. Restricted/security paths and material product, behavior, security, or ownership changes need listed-owner involvement. For ownership/review governance, verified active organization-admin direction also qualifies; repository admin/bypass alone does not. Neither route waives enforced reviews.
- Complete the authorized workflow's review/merge gates; resolve substantive findings or explain rejections. Address failures under the best-effort test-failure policy above; distinguish proven unrelated failures from unexplained ones and cite an owning fix when known. Verify remote outcomes before success or cleanup; uncertain writes require reconciliation, not blind retries.
- Stage only intended files and use concise Conventional Commits with verified author/writer identities. Preserve contributor credit; team-session credit requires consented, verified humans and its canonical backlink. A bare URL grants no public mutation authority. Keep PR bodies current with problem, solution, impact, and evidence; use body files/heredocs for shell-sensitive text.

View file

@ -1,5 +1,5 @@
{
"core": 2449,
"channel": 3740,
"plugin": 4330
"plugin": 4331
}

View file

@ -1,4 +1,4 @@
8b9f9600edac3554646c57753462f902706062466a6fd5176778232a52c46cd8 config-baseline.json
b852dd7bc95d5248478373132f942dfa1f93a1ce4f8ab4441da9e7d94e95e008 config-baseline.json
332b2736b83f706e63961b6ac5cd958931da506b1c83f48450341d5425d3fc26 config-baseline.core.json
51c84b118b136dfff84993e3cee0ff76ee06b614842b125d0fde110635fc10b8 config-baseline.channel.json
ccb4e05cc4d2d5d39458f1fd1718044382c4561327a5adf85e89a57911dd022d config-baseline.plugin.json
ab4f44747880f8979d8bbf1ad5d35d9569c44543410f1233d63973449b4fe4f5 config-baseline.plugin.json

View file

@ -18,7 +18,7 @@ meets a condition.
model architectures and inference backends. Sharing the interface does not make
their reasoning ability or probabilities interchangeable.
The role and bundled TypeSafe AI adapter were added after released OpenClaw
The role and TypeSafe AI adapter were added after released OpenClaw
`2026.9.5`. These instructions apply to development checkouts containing those
features and to later releases that include them. See each provider's setup
page for its host requirements.
@ -41,9 +41,10 @@ Configure the provider plugin before selecting its model:
- [ONNX](/plugins/onnx) runs local CPU classifiers in a persistent subprocess.
Follow its development-checkout or compatible-package setup, then explicitly
download the model or prepare a local export. Inference needs no hosted API credential.
- [TypeSafe AI](/plugins/typesafe) connects to hosted Jev inference. Enable the
bundled plugin and configure its protected credential. Evaluations send the
selected evidence to TypeSafe and incur its normal usage charges.
- [TypeSafe AI](/plugins/typesafe) connects to hosted Jev or a local System One
server such as Kev. Install and enable the external plugin, then configure a
protected hosted credential or an explicit loopback `baseUrl`. Hosted
evaluations send the selected evidence to TypeSafe and incur its normal usage charges.
The current plugins declare these model references:
@ -58,8 +59,9 @@ The current plugins declare these model references:
| `onnx/gliner2.5-small-v1` | GLiNER 2.5 Small | Download pinned ONNX artifacts |
| `typesafe/jev-1.13.0` | Jev 1.13.0 | TypeSafe credential |
| `typesafe/jev-latest` | Jev | TypeSafe credential; follows the vendor's latest model |
| `typesafe/kev-latest` | Kev (local server) | Running System One server and explicit loopback URL |
ONNX support is currently an unpublished candidate. Its plugin page explains
Both plugins are currently unpublished candidates. Their setup pages explain
source-checkout use and the packaged host floor. The table describes the plugins' declared models,
not which artifacts or credentials are ready on your machine.

View file

@ -50,7 +50,7 @@ Each entry lists the package, distribution route, and description.
## Core npm package
63 plugins
62 plugins
- **[a2a](/plugins/reference/a2a)** (`@openclaw/a2a`) - included in OpenClaw. A2A v1.0 Agent-to-Agent protocol channel plugin.
@ -164,8 +164,6 @@ Each entry lists the package, distribution route, and description.
- **[tts-local-cli](/plugins/reference/tts-local-cli)** (`@openclaw/tts-local-cli`) - included in OpenClaw. Adds text-to-speech provider support.
- **[typesafe](/plugins/reference/typesafe)** (`@openclaw/typesafe`) - included in OpenClaw. Optional typed evaluations, not a conversational model provider.
- **[vault](/plugins/reference/vault)** (`@openclaw/vault`) - included in OpenClaw. HashiCorp Vault SecretRef provider integration.
- **[vllm](/plugins/reference/vllm)** (`@openclaw/vllm-provider`) - included in OpenClaw. Adds vLLM model provider support to OpenClaw.
@ -180,7 +178,7 @@ Each entry lists the package, distribution route, and description.
## Official external packages
94 plugins
95 plugins
- **[acpx](/plugins/reference/acpx)** (`@openclaw/acpx`) - npm or ClawHub: `clawhub:@openclaw/acpx`. OpenClaw ACP runtime backend with plugin-owned session and transport management.
@ -346,6 +344,8 @@ Each entry lists the package, distribution route, and description.
- **[twitch](/plugins/reference/twitch)** (`@openclaw/twitch`) - npm or ClawHub: `clawhub:@openclaw/twitch`. OpenClaw Twitch channel plugin for chat and moderation workflows.
- **[typesafe](/plugins/reference/typesafe)** (`@openclaw/typesafe`) - npm or ClawHub: `clawhub:@openclaw/typesafe`. Optional typed evaluations, not a conversational model provider.
- **[venice](/plugins/reference/venice)** (`@openclaw/venice-provider`) - npm or ClawHub: `clawhub:@openclaw/venice-provider`. Adds Venice model provider support to OpenClaw.
- **[vercel-ai-gateway](/plugins/reference/vercel-ai-gateway)** (`@openclaw/vercel-ai-gateway-provider`) - npm or ClawHub: `clawhub:@openclaw/vercel-ai-gateway-provider`. Adds Vercel AI Gateway model provider support to OpenClaw.

View file

@ -15,7 +15,7 @@ Optional typed evaluations, not a conversational model provider.
## Distribution
- Package: `@openclaw/typesafe`
- Install route: included in OpenClaw
- Install route: npm or ClawHub: `clawhub:@openclaw/typesafe`
## Surface

View file

@ -153,9 +153,10 @@ sent to the selected provider may incur its normal usage charges. Plugin disable
wins; installing a tool or credential alone does not select a provider. Vendor adapters
own transport and model-specific translation; no vendor is a core dependency.
The [ONNX plugin](/plugins/onnx) supplies local classifiers; the bundled
[TypeSafe AI plugin](/plugins/typesafe) supplies a Jev adapter. Both require
explicit setup and role selection.
The [ONNX plugin](/plugins/onnx) supplies local classifiers; the
[TypeSafe AI plugin](/plugins/typesafe) supplies hosted Jev and local System One
adapters, including Kev. Both plugins require
separate installation, explicit setup, and role selection.
### Calling from a third-party plugin

View file

@ -1,31 +1,54 @@
---
summary: "Use TypeSafe AI's Jev model for optional typed decisions"
summary: "Use hosted Jev or a local System One server for typed decisions"
title: "TypeSafe AI"
read_when:
- Configuring a typed decision model
- Using the TypeSafe evaluation tool
- Running Kev locally through the System One API
---
# TypeSafe AI
The bundled `typesafe` plugin connects OpenClaw's optional decision model role to
TypeSafe AI's Jev models. Its models appear in the separate **Decision** picker,
never in the conversational model picker.
The official external `typesafe` plugin connects OpenClaw's optional decision
model role to TypeSafe AI's hosted Jev models or an explicitly configured local
System One server such as [Kev](https://github.com/jaredpalmer/kev). Its models appear in the separate
**Decision** picker, never in the conversational model picker.
The bundled adapter and decision model role were added after released OpenClaw
`2026.9.5`. Use a development checkout containing these features, or a later
release that includes them; enabling this configuration on `2026.9.5` does not
add the missing API or bundled plugin.
The adapter and decision model role were added after released OpenClaw
`2026.9.5`. Packaged installs require a host and plugin API of at least
`2026.9.6`; the installer rejects older hosts before loading the plugin.
See [Decision models](/concepts/decision-models) for the model role, available
backends, rubric examples, and provider-neutral plugin API.
The plugin is disabled by default. Bundling or enabling it does not select a
The plugin is disabled by default. Installing or enabling it does not select a
decision model or schedule background work.
## Install
TypeSafe AI is packaged separately from core for publication to npm and
ClawHub. Its first publication is pending a supporting release. Once published,
install it from npm on a compatible host:
```sh
openclaw plugins install @openclaw/typesafe
```
To select ClawHub explicitly:
```sh
openclaw plugins install clawhub:@openclaw/typesafe
```
Until a supporting release is available, use a source checkout containing the
decision-provider API and `extensions/typesafe`. Build it with
`pnpm install --frozen-lockfile` and `pnpm build`, then apply the configuration
below. Source-checkout plugins use the host's co-versioned development API;
that does not make the packaged plugin compatible with OpenClaw `2026.9.5`.
## Enable and configure
Create a protected credential in Settings → Secrets, then reference it from the
For hosted Jev, create a protected credential in Settings → Secrets, then reference it from the
plugin configuration. Merge this example into your existing configuration; keep
any other entries in `plugins.allow`.
@ -57,16 +80,116 @@ any other entries in `plugins.allow`.
`agents.defaults.decisionModel`; an empty override disables decisions for that
agent. An unset or empty global role leaves decisions off by default.
The plugin reads the host's prepared SecretRef value for each request. It does
Hosted mode reads the host's prepared SecretRef value for each request. It does
not independently read environment credentials or cache a previous credential.
A missing or unavailable credential makes decisions unavailable. Use the normal
[secret refresh flow](/gateway/secrets) after changing a credential.
Selecting a decision model authorizes supported, otherwise-enabled consumers to
send their selected evidence to TypeSafe and incur its normal usage charges.
send their selected evidence to the configured endpoint. Hosted Jev requests
incur TypeSafe's normal usage charges.
Consumer scheduling and publication permissions remain unchanged. Clearing the
role or explicitly disabling the plugin prevents its use by those consumers.
## Local System One server
### Run Kev
[Kev](https://github.com/jaredpalmer/kev) is an Apache-2.0 family of decision
models that serves the System One API through a persistent Python process.
It supports Apple Silicon and CUDA. The Qwen3-based Kev-0.6B, Kev-4B, and
Kev-8B checkpoints have been tested with this adapter.
For a Mac, start with the Qwen3-based Kev-4B checkpoint. This example requires
Python 3.12+ and [uv](https://docs.astral.sh/uv/), and pins the tested adapter
revision in a local directory. The first server start also downloads its base
model weights:
```sh
git clone https://github.com/jaredpalmer/kev.git
cd kev
git checkout 5f78968927069eaacc3b2bdb688586989b3933ac
uv sync --frozen --extra serve
uv run python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
"jaredpalmer/kev-4b",
revision="c4bfa11b0dc07691884f2d97f1c4c4c05c92e416",
local_dir="models/kev-4b-qwen3",
)
PY
KEV_DTYPE=bf16 uv run --extra serve python -m kev.serve \
--run models/kev-4b-qwen3 --port 8009
```
The newer default Kev-4B checkpoint uses Qwen3.5; its Mac performance differs
from the Qwen3 checkpoint above. Follow the upstream model cards when choosing
another checkpoint. Kev-0.6B uses less memory; Kev-8B trades more memory and
latency for decision quality. All of them use the same OpenClaw model label
for the server you configure below.
From the same Kev checkout in a second terminal, verify the loaded checkpoint
and run Kev's API tests:
```sh
curl --fail http://127.0.0.1:8009/v1/models
KEV_BASE_URL=http://127.0.0.1:8009 \
uv run --extra serve python -m pytest tests/test_api.py -q
```
### Connect OpenClaw
Start your System One server separately, then set `baseUrl` to its loopback
origin and select `typesafe/kev-latest`:
```json5
{
plugins: {
allow: ["typesafe"],
entries: {
typesafe: {
enabled: true,
config: { baseUrl: "http://127.0.0.1:8009" },
},
},
},
agents: {
defaults: { decisionModel: "typesafe/kev-latest" },
},
}
```
Merge the example with existing settings, preserving other allowed plugins.
Omit `apiKey` for local inference. The plugin does not read or send the hosted
credential on this path; remove a retained SecretRef if the host should also
stop preparing it.
The endpoint applies to every request from this plugin, including requests
whose model label names Jev. Model selection does not choose between hosted and
local endpoints. The `kev-latest` label requires `baseUrl` and is never sent to
the hosted TypeSafe endpoint.
`baseUrl` accepts HTTP or HTTPS on `localhost`, `127.0.0.1`, or `[::1]`, with an
optional port and trailing slash. Supply the origin, without `/v1`, credentials,
query, or fragment; the plugin appends `/v1/systemone`. LAN and remote hosts are
not accepted. Ordinary ambient HTTP proxy variables are not used for these
requests; explicitly enabled managed proxy policy still applies.
Kev runs one checkpoint per server process. Its request model label does not
load or switch weights. Choose the checkpoint when starting the server and
inspect `GET /v1/models` to verify it. See Kev's [serving instructions](https://github.com/jaredpalmer/kev#quick-start)
for installation, model selection, and hardware requirements. OpenClaw does not
download weights or start that process. An unavailable server produces an
unavailable decision, without automatically switching to hosted Jev.
For local compatibility, omitted question instructions are sent as `null`.
Structured Score rubric levels are encoded as text; returned legends must match
that transmitted rubric before the original level descriptions are restored in
tool results. Kev's optional nonnegative `latency_ms` field is validated and
removed; all answer types, labels, probabilities, and rubric bounds retain the
same validation as hosted results.
## Decision contract
Consumers call the provider-neutral
@ -94,7 +217,8 @@ not demonstrated accuracy guarantees or permission to act.
The host owns concurrency, circuit health, deadlines, cancellation, and provider
lifecycle. Native decisions have a ten-second maximum; shorter consumer or
plugin timeouts still apply. The adapter shares transport and response validation
with the tool below. Requests use the fixed TypeSafe HTTPS endpoint, reject
with the tool below. Requests use the fixed TypeSafe HTTPS endpoint unless
`baseUrl` selects a local server. Both paths reject
redirects, and do not retry automatically. Consumers decide what to do with
unavailable decisions; caller cancellation must not start fallback work.
@ -106,7 +230,8 @@ It accepts shared `state`, a map of `questions`, and an optional vendor `model`
override. Its TypeSafe-facing question names are `choice`, `score`, and `noul`.
For this tool only, `plugins.entries.typesafe.config.model` supplies the default
vendor model, initially `jev-latest`. It does not override the native
model, initially `jev-latest` for hosted inference or `kev-latest` for local
inference. It does not override the native
`decisionModel` role or select a provider. Pin a model version for reproducible
tool evaluations. `timeoutMs` limits tool requests and caps native requests at
the shorter of this setting and the host's remaining deadline.
@ -118,9 +243,11 @@ publish, send messages, or change durable state.
## Existing external installation
This bundled plugin uses the same `typesafe` plugin ID as the external prototype.
Do not configure two installations as independent providers. Inspect plugin
resolution before switching, preserve existing configuration and credentials,
and use the supported [plugin management flow](/plugins/manage-plugins) to remove
an external override if you want the bundled copy to own the ID. Installing this
change does not delete external plugin files or credentials.
The official package keeps the `typesafe` plugin ID used by the prototype and
earlier development checkouts. Preserve `plugins.entries.typesafe`, its
protected credential, and agent `decisionModel` selections when switching.
Use the supported [plugin management flow](/plugins/manage-plugins) to replace
the old installation, and remove an explicit prototype path from
`plugins.load.paths` if it would override the installed package. Do not configure
two copies as independent providers. Installing the package does not delete
prototype files or credentials.

View file

@ -0,0 +1,28 @@
# TypeSafe AI for OpenClaw
Official external plugin for typed decisions with hosted TypeSafe AI Jev models
or a local Kev System One server.
It provides Choice, Score, and Boolean judgments through OpenClaw's shared
decision-model API, plus the optional `typesafe_evaluate` tool.
Requires OpenClaw and plugin API **2026.9.6 or later**. Released OpenClaw
2026.9.5 does not include the decision API.
```sh
openclaw plugins install @openclaw/typesafe
```
The ClawHub install spec is `clawhub:@openclaw/typesafe`. First publication is
pending a supporting release. Enable the plugin, configure a protected TypeSafe credential, and select
`typesafe/jev-latest` as your agent's `decisionModel`. Evaluations send the
supplied evidence to TypeSafe AI and incur its normal usage charges.
For local Kev, configure `plugins.entries.typesafe.config.baseUrl` with the
server's loopback origin, such as `http://127.0.0.1:8009`, omit `apiKey`, and select
`typesafe/kev-latest`. The plugin calls `/v1/systemone` without a hosted credential.
Start the server separately with your chosen checkpoint; the decision-model
selection labels requests and does not download or load a model.
See the [TypeSafe AI setup guide](https://docs.openclaw.ai/plugins/typesafe) and
[decision-model documentation](https://docs.openclaw.ai/concepts/decision-models)
for configuration, rubrics, and API semantics.

View file

@ -19,14 +19,14 @@ export default definePluginEntry({
name: "typesafe_evaluate",
label: "TypeSafe typed decisions",
description:
"Ask Jev for typed decisions over explicit shared state: classify/select with Choice (2–255 options), rate with Score (2–10 ordered levels; fractional zero-based result), or estimate probability of yes with Noul (optional true/false criteria). Batch independent questions; they cannot see each other’s answers. Instructions and descriptions accept text, JSON objects/arrays, or null. Returns distributions, confidence for Choice/Score, model, and usage—not generated explanations or authorization. Requires credentials; sends supplied data to TypeSafe and may incur API charges.",
"Ask the configured System One model for typed decisions over explicit shared state: classify/select with Choice (2–255 options), rate with Score (2–10 ordered levels; fractional zero-based result), or estimate probability of yes with Noul (optional true/false criteria). Batch independent questions; they cannot see each other’s answers. Instructions and descriptions accept text, JSON objects/arrays, or null. Returns distributions, confidence for Choice/Score, model, and usage—not generated explanations or authorization. Hosted Jev requires credentials and may incur API charges; a configured local Kev endpoint receives supplied data without hosted credentials.",
parameters: EvaluateInput,
outputSchema: EvaluateOutput,
resultContentSource: "network",
async execute(_id, params, signal) {
signal?.throwIfAborted();
const config = resolveRuntimeConfig(api.runtime.config.current());
if (!config.apiKey) {
if (!config.baseUrl && !config.apiKey) {
throw new Error(
"TypeSafe API key is missing. Configure a SecretRef in plugin Settings.",
);

View file

@ -37,16 +37,20 @@
"uiHints": {
"apiKey": {
"label": "TypeSafe API credential",
"help": "Select a SecretRef. Create protected credentials in Settings \u2192 Secrets. Never paste credentials in chat.",
"help": "Hosted Jev only. Select a SecretRef in Settings \u2192 Secrets. Ignored for a local baseUrl. Never paste credentials in chat.",
"sensitive": true
},
"model": {
"label": "Evaluation tool default model",
"help": "Default only for the optional typesafe_evaluate tool. Native decisions use the agent decisionModel selection."
"help": "Default only for typesafe_evaluate: jev-latest for hosted Jev, kev-latest for local System One. Local model labels do not load checkpoints. Native decisions use the agent decisionModel selection."
},
"timeoutMs": {
"label": "Request timeout (ms)",
"advanced": true
},
"baseUrl": {
"label": "Local System One origin",
"help": "Optional loopback origin, such as http://127.0.0.1:8009, for a running Kev server. Omit for hosted Jev. Local requests do not use the TypeSafe credential; the server chooses the loaded checkpoint."
}
},
"configSchema": {
@ -98,13 +102,18 @@
"minLength": 1,
"maxLength": 128,
"pattern": "^[a-zA-Z0-9._/-]+$",
"default": "jev-latest"
"description": "Tool default: jev-latest for hosted Jev, kev-latest for a local endpoint."
},
"timeoutMs": {
"type": "integer",
"minimum": 1000,
"maximum": 60000,
"default": 10000
},
"baseUrl": {
"type": "string",
"maxLength": 128,
"pattern": "^https?://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]{1,5})?/?$"
}
},
"additionalProperties": false
@ -122,6 +131,11 @@
"provider": "typesafe",
"id": "jev-1.13.0",
"name": "Jev 1.13.0"
},
{
"provider": "typesafe",
"id": "kev-latest",
"name": "Kev (local server)"
}
]
}

View file

@ -1,8 +1,11 @@
{
"name": "@openclaw/typesafe",
"version": "2026.9.4",
"private": true,
"version": "2026.9.5",
"description": "OpenClaw TypeSafe typed decisions and optional evaluation tool",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"typebox": "1.3.30"
@ -13,6 +16,23 @@
"openclaw": {
"extensions": [
"./index.ts"
]
],
"install": {
"clawhubSpec": "clawhub:@openclaw/typesafe",
"npmSpec": "@openclaw/typesafe",
"defaultChoice": "npm",
"minHostVersion": ">=2026.9.6"
},
"compat": {
"pluginApi": ">=2026.9.6"
},
"build": {
"bundledDist": false,
"openclawVersion": "2026.9.5"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View file

@ -1,6 +1,6 @@
---
name: typesafe-evaluate
description: Make explicit typed TypeSafe/Jev decisions with the typesafe_evaluate tool.
description: Make explicit typed hosted Jev or local Kev decisions with the typesafe_evaluate tool.
---
# TypeSafe evaluations
@ -8,7 +8,9 @@ description: Make explicit typed TypeSafe/Jev decisions with the typesafe_evalua
Use `typesafe_evaluate` for semantic decisions over explicit supplied state. It
returns typed decisions, not generated explanations or permission to act. If the
tool is unavailable, report that; do not substitute shell/HTTP calls or ask for a
credential in chat. Calls send the supplied data to TypeSafe and may incur charges.
credential in chat. Hosted calls send the supplied data to TypeSafe and may incur
charges. With a configured local System One origin, calls go to that server
without a hosted credential.
Supply `{state, questions}`; see [mixed request example](references/request-example.json).
State and descriptions accept text, JSON objects/arrays, or null. Each question
@ -26,6 +28,8 @@ Only send necessary evidence authorized for sharing. Never include credentials.
Do not silently truncate evidence or split competing Choice options to fit a
request. The plugin bounds JSON to 4 MiB, 262144 nodes, and depth 64; vendor token
limits are separate. A model override is optional; pin a version when comparing runs.
For local Kev, the server's loaded checkpoint determines the model; an override
only labels the request and does not change the checkpoint.
Reported probabilities may be rounded and need not sum exactly to one. Preserve the
vendor-selected label and score; normalization or selecting the largest reported

View file

@ -1,5 +1,6 @@
import type { RuntimeConfig } from "./config.js";
import { evaluationError } from "./errors.js";
import { EvaluationError, evaluationError } from "./errors.js";
import { localInput, parseLocalResult } from "./local.js";
import { parseInput, parseResult } from "./schema.js";
import { requestEvaluation } from "./transport.js";
@ -9,19 +10,30 @@ export async function evaluate(input: unknown, config: RuntimeConfig, signal?: A
throw evaluationError(undefined, true);
}
const parsed = parseInput(input);
if (!config.apiKey) {
const model = parsed.model ?? config.model;
if (model === "kev-latest" && !config.baseUrl) {
throw new EvaluationError(
"Kev requires a local System One server. Configure baseUrl in TypeSafe plugin Settings.",
"unsupported-input",
);
}
if (!config.baseUrl && !config.apiKey) {
throw new Error("TypeSafe API key is missing. Configure a SecretRef in plugin Settings.");
}
try {
const wireInput = config.baseUrl ? localInput(parsed) : parsed;
const response = await requestEvaluation({
body: { ...parsed, model: parsed.model ?? config.model },
apiKey: config.apiKey,
body: { ...wireInput, model },
apiKey: config.baseUrl ? undefined : config.apiKey,
baseUrl: config.baseUrl,
timeoutMs: config.timeoutMs,
signal,
});
signal?.throwIfAborted();
const evaluation = parseResult(response, parsed);
if (JSON.stringify(evaluation).includes(config.apiKey)) {
const evaluation = config.baseUrl
? parseLocalResult(response, wireInput, parsed)
: parseResult(response, parsed);
if (!config.baseUrl && config.apiKey && JSON.stringify(evaluation).includes(config.apiKey)) {
throw new Error("Invalid TypeSafe response.");
}
return { evaluation };

View file

@ -1,8 +1,12 @@
import { Type } from "typebox";
const DEFAULT_MODEL = "jev-latest";
const LOCAL_BASE_URL_PATTERN =
"^https?://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]{1,5})?/?$";
const localBaseUrlPattern = new RegExp(LOCAL_BASE_URL_PATTERN);
export const ConfigSchema = Type.Object(
{
baseUrl: Type.Optional(Type.String({ maxLength: 128, pattern: LOCAL_BASE_URL_PATTERN })),
apiKey: Type.Optional(
Type.Object(
{
@ -23,7 +27,7 @@ export const ConfigSchema = Type.Object(
minLength: 1,
maxLength: 128,
pattern: "^[a-zA-Z0-9._/-]+$",
default: DEFAULT_MODEL,
description: "Tool default: jev-latest for hosted Jev, kev-latest for a local endpoint.",
}),
),
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 60000, default: 10000 })),
@ -31,12 +35,29 @@ export const ConfigSchema = Type.Object(
{ additionalProperties: false },
);
export type RuntimeConfig = { apiKey?: string; model: string; timeoutMs: number };
export type RuntimeConfig = { apiKey?: string; baseUrl?: string; model: string; timeoutMs: number };
/** A configured endpoint grants access to one loopback origin, never arbitrary private hosts. */
export function localBaseUrl(value: unknown): string | undefined {
if (value === undefined) {
return undefined;
}
try {
if (typeof value !== "string" || value !== value.trim() || !localBaseUrlPattern.test(value)) {
throw new Error();
}
return new URL(value).origin;
} catch {
throw new Error(
"Invalid TypeSafe baseUrl; use an http(s) loopback origin without a path, credentials, query, or fragment.",
);
}
}
/** Validate runtime settings and recognize materialized credentials without resolving inputs. */
export function runtimeConfig(config: Record<string, unknown> | undefined): RuntimeConfig {
const key = config?.apiKey;
const model = config?.model ?? DEFAULT_MODEL;
const baseUrl = localBaseUrl(config?.baseUrl);
const model = config?.model ?? (baseUrl ? "kev-latest" : DEFAULT_MODEL);
const timeoutMs = config?.timeoutMs ?? 10000;
if (
typeof model !== "string" ||
@ -48,5 +69,9 @@ export function runtimeConfig(config: Record<string, unknown> | undefined): Runt
) {
throw new Error("Invalid TypeSafe configuration; check plugin Settings.");
}
if (baseUrl) {
return { baseUrl, model, timeoutMs };
}
const key = config?.apiKey;
return { apiKey: typeof key === "string" && key.trim() ? key : undefined, model, timeoutMs };
}

View file

@ -8,6 +8,9 @@ export function resolveRuntimeConfig(
): RuntimeConfig {
const configured = snapshot.plugins?.entries?.typesafe?.config;
const validated = runtimeConfig(configured);
if (validated.baseUrl) {
return validated;
}
const prepared = getPreparedPluginSecretInput("typesafe", "apiKey");
return { ...validated, apiKey: prepared.value };
}

View file

@ -13,11 +13,14 @@ export function createDecisionProvider(getConfig: () => RuntimeConfig): Decision
return {
id: "typesafe",
contractVersion: 1,
isReady: () => Boolean(getConfig().apiKey),
isReady: () => {
const config = getConfig();
return Boolean(config.baseUrl || config.apiKey);
},
async evaluate(batch: DecisionBatch, context) {
context.signal.throwIfAborted();
const config = getConfig();
if (!config.apiKey) {
if (!config.baseUrl && !config.apiKey) {
return { status: "unavailable", reason: "credentials-unavailable" };
}
const remaining = context.deadlineMonotonicMs - performance.now();

View file

@ -0,0 +1,337 @@
import assert from "node:assert/strict";
import { lookup } from "node:dns/promises";
import { createServer } from "node:http";
import type { DecisionProviderV1 } from "openclaw/plugin-sdk/decisions";
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { getPreparedPluginSecretInput } from "openclaw/plugin-sdk/secret-input-runtime";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import plugin from "../index.js";
import { evaluate } from "./client.js";
import { runtimeConfig } from "./config.js";
import type { EvaluationInput } from "./schema.js";
vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({
getPreparedPluginSecretInput: vi.fn(),
}));
vi.mock("node:dns/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:dns/promises")>();
return { ...actual, lookup: vi.fn(actual.lookup) };
});
const baseUrl = "http://127.0.0.1:8009";
const input = {
state: { message: "Synthetic outage" },
questions: {
c: { type: "choice", criteria: { support: "Service outages", sales: null } },
s: { type: "score", criteria: [null, { impact: ["widespread", true, 2] }] },
b: { type: "noul", instructions: { question: "Escalate?" } },
},
} satisfies EvaluationInput;
const localAnswer = {
model: "kev-latest",
answers: {
c: {
type: "choice",
choice: "support",
confidence: 0.8,
probabilities: { support: 0.9, sales: 0.1 },
},
s: {
type: "score",
score: 0.8,
confidence: 0.8,
probabilities: { 0: 0.2, 1: 0.8 },
legend: { 0: "", 1: '{"impact":["widespread",true,2]}' },
},
b: { type: "noul", noul: 0.75 },
},
usage: { input_tokens: 30, output_tokens: 12 },
latency_ms: 15.7,
};
beforeEach(() => {
vi.mocked(getPreparedPluginSecretInput).mockReset();
vi.mocked(getPreparedPluginSecretInput).mockReturnValue({ revision: 1, value: "hosted-secret" });
});
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.mocked(lookup).mockReset();
});
it("runs the registered tool and decision provider locally without reading hosted credentials", async () => {
const fetch = vi.fn(
async (_url: RequestInfo | URL, _init?: RequestInit) =>
new Response(JSON.stringify(localAnswer)),
);
vi.stubGlobal("fetch", fetch);
vi.stubEnv("HTTP_PROXY", "http://proxy.invalid:3128");
vi.stubEnv("HTTPS_PROXY", "http://proxy.invalid:3128");
vi.stubEnv("ALL_PROXY", "http://proxy.invalid:3128");
vi.stubEnv("NO_PROXY", "");
const registerTool = vi.fn<OpenClawPluginApi["registerTool"]>();
const registerDecisionProvider = vi.fn<OpenClawPluginApi["registerDecisionProvider"]>();
plugin.register({
runtime: {
config: {
current: () => ({
plugins: {
entries: { typesafe: { config: { baseUrl, apiKey: "hosted-materialized" } } },
},
}),
},
},
registerTool,
registerDecisionProvider,
} as unknown as OpenClawPluginApi);
const tool = registerTool.mock.calls[0]?.[0] as AnyAgentTool;
const provider = registerDecisionProvider.mock.calls[0]?.[0] as DecisionProviderV1;
expect(provider.isReady?.()).toBe(true);
const result = await tool.execute("local-test", input);
expect(result.details).toEqual({
evaluation: {
model: "kev-latest",
usage: localAnswer.usage,
answers: {
...localAnswer.answers,
s: {
...localAnswer.answers.s,
legend: { 0: null, 1: input.questions.s.criteria[1] },
},
},
},
});
await expect(
provider.evaluate(
{
...input,
questions: { ...input.questions, b: { ...input.questions.b, type: "boolean" } },
},
{
model: "kev-latest",
signal: new AbortController().signal,
deadlineMonotonicMs: performance.now() + 1000,
},
),
).resolves.toMatchObject({
status: "ok",
result: {
answers: {
c: localAnswer.answers.c,
s: { type: "score", score: 0.8, probabilities: [0.2, 0.8] },
b: { type: "boolean", probabilityTrue: 0.75 },
},
},
});
expect(getPreparedPluginSecretInput).not.toHaveBeenCalled();
expect(fetch).toHaveBeenCalledTimes(2);
for (const [url, init] of fetch.mock.calls) {
expect(url).toBe(`${baseUrl}/v1/systemone`);
expect(new Headers(init?.headers).has("authorization")).toBe(false);
assert(typeof init?.body === "string");
expect(JSON.parse(init.body)).toEqual({
...input,
model: "kev-latest",
questions: {
c: { ...input.questions.c, instructions: null },
s: {
...input.questions.s,
instructions: null,
criteria: ["", '{"impact":["widespread",true,2]}'],
},
b: input.questions.b,
},
});
}
});
it("refuses to send the local Kev selection to hosted inference through registered handlers", async () => {
const fetch = vi.fn(
async () =>
new Response(
JSON.stringify({
model: "kev-latest",
answers: { q: { type: "noul", noul: 0.75 } },
usage: localAnswer.usage,
}),
),
);
vi.stubGlobal("fetch", fetch);
const registerTool = vi.fn<OpenClawPluginApi["registerTool"]>();
const registerDecisionProvider = vi.fn<OpenClawPluginApi["registerDecisionProvider"]>();
plugin.register({
runtime: { config: { current: () => ({}) } },
registerTool,
registerDecisionProvider,
} as unknown as OpenClawPluginApi);
const provider = registerDecisionProvider.mock.calls[0]?.[0];
assert(provider);
await expect(
provider.evaluate(
{ state: "local-only evidence", questions: { q: { type: "boolean" } } },
{
model: "kev-latest",
signal: new AbortController().signal,
deadlineMonotonicMs: performance.now() + 10000,
},
),
).resolves.toEqual({ status: "unavailable", reason: "unsupported-input" });
const tool = registerTool.mock.calls[0]?.[0] as AnyAgentTool;
await expect(
tool.execute("local-test", {
state: "local-only evidence",
questions: { q: { type: "noul" } },
model: "kev-latest",
}),
).rejects.toThrow("baseUrl");
expect(fetch).not.toHaveBeenCalled();
});
it.each([
"http://localhost:8009",
"http://127.0.0.1:8009/",
"http://[::1]:8009",
"https://localhost",
])("accepts an explicit loopback origin %s", (url) => {
expect(runtimeConfig({ baseUrl: url, apiKey: "ignored-secret" })).toEqual({
baseUrl: new URL(url).origin,
model: "kev-latest",
timeoutMs: 10000,
});
});
it.each([
"",
"http://192.168.1.2:8009",
"https://remote.example",
"http://localhost.example",
"http://localhost:8009/v1",
"http://localhost:8009?x=1",
"http://localhost:8009#x",
"http://user:password@localhost:8009",
"file:///localhost",
"http://localhost:65536",
"http://127.1:8009",
"http://2130706433:8009",
"http://localhost.:8009",
null,
8009,
])("rejects non-origin, non-loopback, or ambiguous endpoint %s", (url) => {
expect(() => runtimeConfig({ baseUrl: url })).toThrow("baseUrl");
});
it.each([
{ ...localAnswer, latency_ms: -1 },
{ ...localAnswer, latency_ms: "15" },
{ ...localAnswer, debug: "extra metadata" },
{
...localAnswer,
answers: {
...localAnswer.answers,
s: {
...localAnswer.answers.s,
legend: { 0: "", 1: "wrong rubric" },
},
},
},
{ ...localAnswer, answers: { ...localAnswer.answers, b: { type: "noul", noul: 2 } } },
])("keeps answer and metadata validation strict for local responses", async (response) => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(JSON.stringify(response))),
);
await expect(evaluate(input, runtimeConfig({ baseUrl }))).rejects.toThrow("invalid response");
});
it("keeps hosted response validation strict and ignores stale keys even on direct local calls", async () => {
const fetch = vi.fn(
async (_url: RequestInfo | URL, _init?: RequestInit) =>
new Response(JSON.stringify(localAnswer)),
);
vi.stubGlobal("fetch", fetch);
await expect(
evaluate(input, {
...runtimeConfig({ baseUrl }),
apiKey: "hosted-secret",
}),
).resolves.toHaveProperty("evaluation.model", "kev-latest");
expect(new Headers(fetch.mock.calls[0]?.[1]?.headers).has("authorization")).toBe(false);
const hostedInput = {
...input,
questions: {
...input.questions,
s: {
...input.questions.s,
criteria: ["", '{"impact":["widespread",true,2]}'],
},
},
};
await expect(evaluate(hostedInput, runtimeConfig({ apiKey: "hosted-secret" }))).rejects.toThrow(
"invalid response",
);
expect(fetch.mock.calls[1]?.[0]).toBe("https://api.typesafe.ai/v1/systemone");
expect(new Headers(fetch.mock.calls[1]?.[1]?.headers).get("authorization")).toBe(
"Bearer hosted-secret",
);
});
it.each(["127.0.0.1", "localhost", "[::1]"])(
"reaches a real %s server without ambient proxies or localhost DNS",
async (hostname) => {
const received: { url?: string; authorization?: string; host?: string; body: string }[] = [];
const server = createServer((request, response) => {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
received.push({
url: request.url,
authorization: request.headers.authorization,
host: request.headers.host,
body: Buffer.concat(chunks).toString("utf8"),
});
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(localAnswer));
});
});
await new Promise<void>((resolve) => {
server.listen(0, hostname === "[::1]" ? "::1" : "127.0.0.1", resolve);
});
try {
const address = server.address();
assert(address && typeof address === "object");
for (const name of [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
]) {
vi.stubEnv(name, "http://127.0.0.1:1");
}
vi.stubEnv("NO_PROXY", "");
vi.stubEnv("no_proxy", "");
vi.stubEnv("OPENCLAW_DEBUG_PROXY_ENABLED", "false");
if (hostname === "localhost") {
vi.mocked(lookup).mockRejectedValue(new Error("Synthetic untrusted localhost resolver"));
}
await expect(
evaluate(input, runtimeConfig({ baseUrl: `http://${hostname}:${address.port}` })),
).resolves.toHaveProperty("evaluation.answers.b.noul", 0.75);
expect(received).toHaveLength(1);
expect(received[0]).toMatchObject({
url: "/v1/systemone",
authorization: undefined,
host: `${hostname}:${address.port}`,
});
expect(JSON.parse(received[0]!.body).questions.c.instructions).toBeNull();
if (hostname === "localhost") {
expect(lookup).not.toHaveBeenCalled();
}
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
},
);

View file

@ -0,0 +1,49 @@
import { parseResult, type Evaluation, type EvaluationInput } from "./schema.js";
export function localInput(input: EvaluationInput): EvaluationInput {
const questions: EvaluationInput["questions"] = {};
for (const [id, question] of Object.entries(input.questions)) {
const instructions = question.instructions ?? null;
if (question.type === "score") {
questions[id] = {
...question,
instructions,
// Send explicit text so the returned legend can be verified without reproducing Kev's renderer.
criteria: question.criteria.map((level) =>
typeof level === "string" ? level : level === null ? "" : JSON.stringify(level),
),
};
} else {
questions[id] = { ...question, instructions };
}
}
return { ...input, questions };
}
export function parseLocalResult(
value: unknown,
wireInput: EvaluationInput,
originalInput: EvaluationInput,
): Evaluation {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Invalid local System One response.");
}
let result = value;
if ("latency_ms" in value) {
const { latency_ms, ...payload } = value;
if (typeof latency_ms !== "number" || !Number.isFinite(latency_ms) || latency_ms < 0) {
throw new Error("Invalid local System One latency metadata.");
}
result = payload;
}
const evaluation = parseResult(result, wireInput);
for (const [id, question] of Object.entries(originalInput.questions)) {
const answer = evaluation.answers[id];
if (question.type === "score" && answer?.type === "score") {
answer.legend = Object.fromEntries(
question.criteria.map((level, index) => [String(index), level]),
);
}
}
return evaluation;
}

View file

@ -68,6 +68,7 @@ describe("plugin ownership and configuration", () => {
expect(manifest.decisionModels).toEqual([
{ provider: "typesafe", id: "jev-latest", name: "Jev" },
{ provider: "typesafe", id: "jev-1.13.0", name: "Jev 1.13.0" },
{ provider: "typesafe", id: "kev-latest", name: "Kev (local server)" },
]);
expect(manifest.providers).toBeUndefined();
expect(manifest.modelCatalog).toBeUndefined();

View file

@ -3,7 +3,7 @@ import { Type, type Static, type TSchema } from "typebox";
import { Compile } from "typebox/compile";
import { Check } from "typebox/value";
// Transport/CPU guards, not Jev token limits. Jev enforces its own context budget.
// Transport/CPU guards; each server enforces its model's context budget.
export const MAX_JSON_BYTES = 4 * 1024 * 1024;
const MAX_JSON_NODES = 262144;
const MAX_JSON_DEPTH = 64;
@ -31,7 +31,7 @@ const entry = Type.Union(
const instructions = Type.Optional(
Type.Union(entry.anyOf, {
description:
"The complete judgment to make; question IDs are not read by Jev. Text, structured object/array, or null. May be omitted when criteria express the judgment.",
"The complete judgment to make; question IDs are not read by the model. Text, structured object/array, or null. May be omitted when criteria express the judgment.",
}),
);
const model = Type.String({ minLength: 1, maxLength: 128, pattern: "^[a-zA-Z0-9._/-]+$" });
@ -102,13 +102,13 @@ export const EvaluateInput = Type.Object(
questions: map(question, {
minProperties: 1,
description:
"Nonempty map of question IDs to Choice, Score, or Noul questions. Mix types in one call. IDs only match answers; put all meaning in instructions/criteria. Questions are independent. Jev enforces token limits; plugin JSON guard is 4 MiB.",
"Nonempty map of question IDs to Choice, Score, or Noul questions. Mix types in one call. IDs only match answers; put all meaning in instructions/criteria. Questions are independent. The server enforces token limits; plugin JSON guard is 4 MiB.",
}),
model: Type.Optional(
Type.String({
...model,
description:
"Optional Jev model ID or alias for this explicit tool call; defaults to the plugin’s evaluation-tool model. Native decisions use the host-selected model.",
"Optional System One model ID or alias for this explicit tool call; defaults to the plugin’s evaluation-tool model. Native decisions use the host-selected model. A local Kev server uses its loaded checkpoint regardless of this label.",
}),
),
},

View file

@ -6,6 +6,7 @@ import {
} from "openclaw/plugin-sdk/fetch-runtime";
import { parseRetryAfterHeaderSeconds } from "openclaw/plugin-sdk/retry-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { localBaseUrl } from "./config.js";
import { EvaluationError } from "./errors.js";
import { MAX_JSON_BYTES, type EvaluationInput } from "./schema.js";
@ -78,10 +79,13 @@ async function readBody(response: Response, signal?: AbortSignal): Promise<Buffe
export async function requestEvaluation(params: {
body: EvaluationInput & { model: string };
apiKey: string;
apiKey?: string;
baseUrl?: string;
timeoutMs: number;
signal?: AbortSignal;
}): Promise<unknown> {
const baseUrl = localBaseUrl(params.baseUrl);
const endpoint = baseUrl ? `${baseUrl}/v1/systemone` : ENDPOINT;
const body = JSON.stringify(params.body);
if (Buffer.byteLength(body) > MAX_JSON_BYTES) {
throw new EvaluationError("TypeSafe request exceeds its limit.", "unsupported-input");
@ -94,16 +98,26 @@ export async function requestEvaluation(params: {
try {
signal?.throwIfAborted();
const request = {
url: ENDPOINT,
url: endpoint,
fetchImpl: globalThis.fetch,
requireHttps: true,
requireHttps: !baseUrl,
...(baseUrl ? { policy: { allowedOrigins: [baseUrl] } } : {}),
...(baseUrl && new URL(baseUrl).hostname === "localhost"
? {
// Keep localhost local even when system DNS or hosts entries override its meaning.
lookupFn: async () => [
{ address: "127.0.0.1", family: 4 },
{ address: "::1", family: 6 },
],
}
: {}),
maxRedirects: 0,
signal,
beforeRequest: () => signal?.throwIfAborted(),
init: {
method: "POST",
headers: {
Authorization: `Bearer ${params.apiKey}`,
...(!baseUrl ? { Authorization: `Bearer ${params.apiKey}` } : {}),
Accept: "application/json",
"Content-Type": "application/json",
},
@ -111,7 +125,7 @@ export async function requestEvaluation(params: {
},
};
const guarded = await fetchWithSsrFGuard(
shouldUseEnvHttpProxyForUrl(ENDPOINT)
!baseUrl && shouldUseEnvHttpProxyForUrl(endpoint)
? withTrustedEnvProxyGuardedFetchMode(request)
: request,
);

View file

@ -361,6 +361,7 @@
"!dist/extensions/tokenjuice/**",
"!dist/extensions/tlon/**",
"!dist/extensions/twitch/**",
"!dist/extensions/typesafe/**",
"!dist/extensions/venice/**",
"!dist/extensions/vercel-ai-gateway/**",
"!dist/extensions/visitor-access/**",

View file

@ -825,6 +825,24 @@
}
}
},
{
"name": "@openclaw/typesafe",
"description": "OpenClaw TypeSafe typed decisions and optional evaluation tool",
"source": "official",
"kind": "plugin",
"openclaw": {
"plugin": {
"id": "typesafe",
"label": "TypeSafe AI"
},
"install": {
"clawhubSpec": "clawhub:@openclaw/typesafe",
"npmSpec": "@openclaw/typesafe",
"defaultChoice": "npm",
"minHostVersion": ">=2026.9.6"
}
}
},
{
"name": "@openclaw/voice-call",
"description": "OpenClaw voice-call plugin",