Files
OpenViking/agent-plugins/README.md
T
t0saki 0ec8d25996 fix(plugins): resolve one connection for every plugin's hooks and MCP proxy (#5132)
* fix(codex): read the MCP proxy's connection from the hooks' loadConfig

The proxy resolved url, api_key, account and user through the bare
credential chain while taking everything else from loadConfig(). Since
the loader became buildPluginConfig() it adds layers that chain never
sees: ovcli.conf's plugin.codex apiKey/accountId/userId, and ov.conf's
codex.apiKey when ovcli.conf names only the server. With either, hooks
authenticated and every MCP tool call went out without a key.

Codex also hands a stdio MCP server only the env vars .mcp.json lists,
and OPENVIKING_AUTH_MODE was not one of them, so an env-set auth mode
decided the identity headers for hooks but not for MCP calls.

* fix(dsh): forward the resolved auth mode and timeout to the MCP proxy

The proxy runs as a child whose env DSH scrubs, so the parent forwards
what it resolved. It forwarded the endpoint, key, account, user and
peer but not the auth mode or request timeout, so a Cordis patch that
set either configured the in-process runtime and not the MCP calls.

The proxy now also takes its credential source and watched paths from
the resolved config instead of a second credential-chain call, and a
shared test keeps every proxy that ships beside hooks off that chain.

* refactor(shared): one connection resolver for hooks and the MCP proxy

The credential chain lived in two layers. `resolveOpenVikingCredentials()`
could not read ovcli.conf's `plugin.<harness>` keys, the ov.conf harness
fallback or the root-key tail; `buildPluginConfig()` patched those in, and
any caller that used the lower layer alone resolved a different key and
identity than the hooks did.

`resolveConnection(harness, { env, files, hostInput, rootKeyFallback })`
now answers server, key, identity and auth mode in one place, reading only
host input, the environment and the two ~/.openviking files. The hook
loader and `buildProxyConnection()` both consume it, and the old
two-layer entry points (`resolveOpenVikingCredentials`, `resolveAuthMode`,
the credentials.mjs CLI) are gone so a half-resolved chain cannot be
written again.

Behaviour is unchanged for every hook harness (checked field by field
against the previous implementation over thousands of generated file/env
combinations). Two deliberate additions: the portable agent-plugins proxy
now honours ovcli.conf's `plugin` connection keys and ov.conf's harness
section like every other harness, and dsh hands the host's `authMode`
(or `auth_mode`) over as host input, ranking it with the host's endpoint,
key and identity.

* fix(shared): a forced env credential source reads only the environment

`OPENVIKING_CREDENTIAL_SOURCE=env` is documented as "env vars only", but
only the url honoured it: the key, account, user, ovcli.conf's actor peer
and the auth mode still fell through to ovcli.conf, its plugin keys,
ov.conf and the root key when the variable was unset. A process that
exported an empty key to mean "no key" was silently handed whatever the
files held.

Forced to `env`, the connection now reads no file and an unset variable
stays empty; the url defaults to http://127.0.0.1:1933. The `peerId`
setting keeps its own layers. The doctor labels that mode instead of
pointing at files the chain skipped.

* refactor(shared): one proxy-config mapper and one forwarded-env list

Each proxy entrypoint copied a dozen fields out of its loader by hand,
under two sets of names, and the copies had drifted. What the proxy
process must be handed was a second hand-kept list, in Codex's
`.mcp.json` and in its test.

`toMcpProxyConfig(cfg, options)` maps a resolved loader or proxy
connection to the proxy config once. `MCP_PROXY_ENV_VARS` names every
variable that changes what a proxy sends; Codex's `env_vars` is now
checked against it, which adds the missing `OPENVIKING_STATE_DIR`.

* refactor(plugins): every loader takes an env, every proxy exports readProxyConfig(env)

The six MCP proxy entrypoints now reduce to one line: resolve through the
harness's own loader (or `buildProxyConnection` for the hook-less package)
and hand the result to `toMcpProxyConfig`. Every one exports
`readProxyConfig(env)`, and the codex, claude-code, opencode and agent-hook
loaders accept an injected env, so a test can drive a hook and its proxy
from the same inputs without touching process.env.

Mapping through one function fixes what the hand copies had lost: the
Claude Code and DSH proxies never passed `mcpUrl`, so
`OPENVIKING_MCP_URL` moved the hooks and left the tools behind.

The source guard now requires the shared mapper and the exported reader
in every proxy, and `buildProxyConnection` reports its two config paths
instead of a watch list of its own.

* fix(dsh): forward the resolved connection to the MCP proxy

DSH starts its MCP subprocess with the parent's environment minus
credential-shaped names (`/KEY|PASSWORD|SECRET|TOKEN/i`), so the bundle
forwards what it resolved. It forwarded the values but not the mode, and
only the non-empty ones:

- A child that receives `OPENVIKING_URL` runs the chain unpinned. Where
  the parent's chain was pinned to an ovcli.conf that names only a url,
  the parent sent no key while the proxy fell through to ov.conf's
  `server.root_api_key`, so the tools reached the server as root while
  the hooks were anonymous.
- `OPENVIKING_ACCOUNT`, `OPENVIKING_USER` and `OPENVIKING_PEER_ID` survive
  DSH's scrub, so a value the parent's chain ignored filled the gap in
  the child and went out as an identity header.
- With no peer to forward, the proxy derived one from its own launch
  directory and sent an actor peer the runtime did not.

`forwardConnectionEnv(connection)` now writes every credential variable,
the empty ones too, with the forced `env` source, so the child reads no
file and resolves exactly the parent's url, MCP url, key, identity, auth
mode and peer. The proxy takes its peer from that environment only.
`buildMcpConfig` moves to `mcp-env.mjs`, which carries no host
dependency, so shared tests can build the child environment without the
DSH bridge.

* test(shared): prove the proxy and the hooks resolve one connection

The existing guards checked shape — that a proxy called the shared
builder — never that it reached the server as the same caller its hooks
did, which is how two harnesses shipped proxies that disagreed with them.

`mcp-hook-parity.test.mjs` runs every harness that ships a proxy beside
hooks through a dozen configurations: ovcli.conf's own fields, its
`plugin.<harness>` and shared plugin keys, ov.conf-only installs, the
pinned fallbacks to a harness key and to the root key, credential and
auth-mode variables, a forced source over stale variables, an explicit
MCP URL, a host's own input, and a workspace file that tries to move the
connection. The hook loader sees the full environment; the proxy sees
only what its host lets through — Codex's `env_vars`, DSH's scrubbed
inheritance plus the forwarded connection, everyone else's full
environment — and the url, key, identity and identity-header switch they
put on the wire must match. Scenarios with a known answer pin it too, and
a coverage check fails when a new proxy or hook client has no row.

The two codex-only proxy tests the matrix now covers are removed.

* docs(plugins): one connection for hooks and MCP, and version bumps

The capability reference, plugin development guide, Agent Plugins and
Codex pages (en/zh), both doctor references and the plugin READMEs now
describe the chain `resolveConnection()` runs: host input first, the
pinned ovcli.conf branch and what still falls through it, the auth mode
reading `OPENVIKING_AUTH_MODE` and the `plugin` keys in every mode, a
forced `env` source reading no file, and the two ways a connection crosses
into an MCP process (Codex's forwarded-variable list, dsh's forwarded
connection). The parity test is registered with the credential tests.

Versions move past both this branch's base and main: claude-code 0.5.2,
codex 0.9.2, agent-hook 0.3.2, opencode 0.3.2, dsh 0.4.3, pi 0.3.2,
agent-plugins 0.1.2.
2026-09-17 19:38:18 +08:00

6.2 KiB

OpenViking Agent Plugins (Agent Plugins 1.0)

Portable Agent Plugins 1.0 package for OpenViking: long-term semantic memory and context for coding agents.

Agent Plugins 1.0 is a vendor-neutral packaging format for extending AI coding agents, backed by Amazon, Cursor, Microsoft, OpenAI, and Vercel among others. A plugin is a plain directory with a plugin.json manifest, auto-discovered Agent Skills under skills/, and optional MCP server declarations in mcp.json — one package that any conforming client (Cursor, VS Code, Amazon- and OpenAI-side clients, ...) can load the same way. This directory is that package for OpenViking.

What's inside

plugin.json                          # Agent Plugins 1.0 manifest
mcp.json                             # stdio MCP server: "openviking"
servers/mcp-proxy.mjs                # stdio -> streamable-HTTP proxy to the OV server's /mcp
servers/shared/                      # generated from examples/memory-plugin-shared/lib (do not edit)
skills/openviking-memory/SKILL.md    # teaches the model the recall + persist loop
skills/ov-memory-troubleshoot/       # read-only extraction troubleshooting
plugin.test.mjs                      # node --test conformance checks

Use ov-memory-troubleshoot to trace backward from a memory file to its archive diff and, when needed, session messages. Diagnosis is read-only.

Zero npm dependencies; the proxy and tests run on the Node.js standard library (Node 18+ for global fetch).

Install

  1. Have an OpenViking server reachable (see the quickstart); default local endpoint is http://127.0.0.1:1933.
  2. Point your Agent-Plugins-conforming client at this directory (each client has its own install command or plugin directory; consult its docs). The client will:
    • register the openviking MCP server from mcp.json — it runs node <plugin>/servers/mcp-proxy.mjs over stdio;
    • discover the openviking-memory and ov-memory-troubleshoot skills from skills/.
  3. Configure credentials (next section) and start a session. The model gains find / search / read / remember / write and the other OpenViking MCP tools. Use search with mode="context" for server-assembled context.

Why a stdio proxy instead of a streamable-http entry

OpenViking already speaks streamable HTTP at /mcp, but a streamable-http entry in mcp.json cannot work portably: the server URL is per-deployment (localhost for one user, a remote endpoint for another), and the Agent Plugins spec forbids credentials in the static headers map. The stdio proxy solves both — it resolves the URL and API key at runtime from the same local sources as the ov CLI, injects them per request, and forwards JSON-RPC over streamable HTTP unchanged.

Credential resolution

Highest to lowest priority (same chain as the ov CLI and the other OpenViking plugins):

  1. Environment variables: OPENVIKING_URL (or OPENVIKING_BASE_URL), OPENVIKING_API_KEY (or OPENVIKING_BEARER_TOKEN), OPENVIKING_ACCOUNT, OPENVIKING_USER, OPENVIKING_PEER_ID
  2. ~/.openviking/ovcli.conf (url, api_key, account, user, then the plugin.agent_plugins and plugin keys) — override the path with OPENVIKING_CLI_CONFIG_FILE
  3. ~/.openviking/ov.conf agent_plugins section, then its server section (url or host/port, root_api_key) — override the path with OPENVIKING_CONFIG_FILE
  4. Defaults: http://127.0.0.1:1933, no auth (local mode)

Config file changes are picked up by the running proxy without a restart. Debugging: set OPENVIKING_DEBUG=1 to log JSON lines to ~/.openviking/logs/agent-plugins.log (path override: OPENVIKING_DEBUG_LOG).

Scope: what this package does and doesn't do

This package is the portable recall + write surface: skills plus MCP tools, driven by the model. Agent Plugins 1.0 deliberately excludes hooks, commands, and agents, so automatic conversation capture and automatic pre-prompt recall are out of scope here — the skills/openviking-memory skill instead teaches the model to recall at task start and persist durable facts via remember/write itself.

If your harness has a hook system, prefer the dedicated plugin — hook-driven recall and capture cost no tool calls and don't depend on the model choosing to remember. One installer covers Claude Code, Codex, Cursor, TRAE / TRAE CN, ZCode, OpenCode, and pi; it prompts for language, harnesses, download source, and credentials, and is idempotent:

bash <(curl -fsSL https://raw.githubusercontent.com/volcengine/OpenViking/main/examples/memory-plugin-shared/install.sh)
# GitHub hard to reach? Same installer from the Volcengine TOS mirror:
bash <(curl -fsSL https://ovrelease.tos-cn-beijing.volces.com/memory-plugin-shared/install.sh)

Use this Agent Plugins package for harnesses with no hooks, or when you want one package that loads across many clients.

Per the spec, client-specific integrations may later be embedded in this package under reverse-domain namespaced directories (e.g. com.example.client/) or the manifest's extensions object without breaking other clients.

Development

node --test agent-plugins/plugin.test.mjs

servers/shared/*.mjs are generated, verbatim copies from examples/memory-plugin-shared/lib — do not edit them here. This directory is a target of the shared-lib sync script, so refresh them with:

node examples/memory-plugin-shared/sync.mjs

examples/memory-plugin-shared/sync.test.mjs fails if they drift, and pins this package to the connection half of the shared runtime: servers/mcp-proxy.mjs resolves everything through buildProxyConnection() in shared/credentials.mjs — the same resolveConnection() every other plugin's hooks and proxy use — so the hook-tuning knobs — which this spec has no hooks to run — never enter the bundle.

Both test files run in CI via .github/workflows/pr.yml.