From 2aed7dfde32f2de0171c09cd43e236f9eb6039de Mon Sep 17 00:00:00 2001 From: Spencer Charest Date: Sun, 19 Jul 2026 11:01:27 -0700 Subject: [PATCH 1/2] feat(hermes): promote Hermes Agent to plugin install from repo subdirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hermes >= v2026.6.19 installs plugins from a repo subdirectory (hermes plugins install owner/repo/path --enable), so the plugin ships from plugins/zero-hermes/hermes/ directly — no dedicated mirror repo. The shared ensure-runner hook and skill are vendored into the plugin directory by scripts/sync-hermes.sh (subdir installs ship only that directory); CI fails on drift. Native 'hermes plugins update' does not apply to subdir installs (no .git is kept), so the --force reinstall is the documented update path. Co-Authored-By: Claude Fable 5 --- .github/workflows/hermes-plugin-sync.yml | 35 ++ Makefile | 9 +- README.md | 2 +- guides/agent-install.md | 22 +- guides/hermes-agent.md | 36 ++ plugins/zero-gemini/gemini-extension.json | 2 +- plugins/zero-hermes/README.md | 44 ++ plugins/zero-hermes/hermes/__init__.py | 103 +++++ .../zero-hermes/hermes/hooks/ensure-runner.sh | 400 +++++++++++++++++ plugins/zero-hermes/hermes/mcp-zero.yaml | 7 + plugins/zero-hermes/hermes/plugin.yaml | 11 + .../zero-hermes/hermes/skills/zero/SKILL.md | 403 ++++++++++++++++++ plugins/zero-hermes/manifest.json | 8 + plugins/zero/.claude-plugin/plugin.json | 2 +- plugins/zero/.codex-plugin/plugin.json | 2 +- plugins/zero/.factory-plugin/plugin.json | 2 +- plugins/zero/agents.json | 7 + scripts/sync-hermes.sh | 75 ++++ 18 files changed, 1154 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/hermes-plugin-sync.yml create mode 100644 guides/hermes-agent.md create mode 100644 plugins/zero-hermes/README.md create mode 100644 plugins/zero-hermes/hermes/__init__.py create mode 100755 plugins/zero-hermes/hermes/hooks/ensure-runner.sh create mode 100644 plugins/zero-hermes/hermes/mcp-zero.yaml create mode 100644 plugins/zero-hermes/hermes/plugin.yaml create mode 100644 plugins/zero-hermes/hermes/skills/zero/SKILL.md create mode 100644 plugins/zero-hermes/manifest.json create mode 100755 scripts/sync-hermes.sh diff --git a/.github/workflows/hermes-plugin-sync.yml b/.github/workflows/hermes-plugin-sync.yml new file mode 100644 index 0000000..53b4aed --- /dev/null +++ b/.github/workflows/hermes-plugin-sync.yml @@ -0,0 +1,35 @@ +name: Hermes plugin sync + +# The Hermes plugin installs straight from this repo's subdirectory +# (plugins/zero-hermes/hermes/), and a subdirectory install ships only what is +# checked in there — it cannot reference plugins/zero/ at runtime. The shared +# hook script and skill are therefore vendored into the plugin directory by +# scripts/sync-hermes.sh. This check fails any PR where the vendored copies +# drift from their sources. +on: + pull_request: + paths: + - 'plugins/zero/hooks/ensure-runner.sh' + - 'plugins/zero/skills/zero/SKILL.md' + - 'plugins/zero-hermes/**' + - 'scripts/sync-hermes.sh' + push: + branches: [main] + paths: + - 'plugins/zero/hooks/ensure-runner.sh' + - 'plugins/zero/skills/zero/SKILL.md' + - 'plugins/zero-hermes/**' + - 'scripts/sync-hermes.sh' + +permissions: + contents: read + +jobs: + hermes-plugin-sync: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify vendored Hermes plugin files are in sync + run: scripts/sync-hermes.sh --check diff --git a/Makefile b/Makefile index 8c75263..05ac76a 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,9 @@ MANIFESTS := \ plugins/zero/.claude-plugin/plugin.json \ plugins/zero/.codex-plugin/plugin.json \ plugins/zero/.factory-plugin/plugin.json \ - plugins/zero-gemini/gemini-extension.json + plugins/zero-gemini/gemini-extension.json \ + plugins/zero-hermes/manifest.json \ + plugins/zero-hermes/hermes/plugin.yaml # The manifest read for the current version (all of MANIFESTS stay in lockstep). VERSION_SOURCE := plugins/zero/.claude-plugin/plugin.json @@ -58,9 +60,10 @@ bump: next="$$major.$$minor.$$patch"; \ for m in $(MANIFESTS); do \ [ -f "$$m" ] || { echo "make: missing manifest: $$m" >&2; exit 1; }; \ - grep -q '"version"' "$$m" || { echo "make: no \"version\" key in $$m" >&2; exit 1; }; \ + grep -Eq '"version"|^version:' "$$m" || { echo "make: no version key in $$m" >&2; exit 1; }; \ tmp=$$(mktemp); \ - sed 's/\("version"[[:space:]]*:[[:space:]]*"\)[^"]*"/\1'"$$next"'"/' "$$m" > "$$tmp" && mv "$$tmp" "$$m"; \ + sed -e 's/\("version"[[:space:]]*:[[:space:]]*"\)[^"]*"/\1'"$$next"'"/' \ + -e 's/^version:[[:space:]]*.*/version: '"$$next"'/' "$$m" > "$$tmp" && mv "$$tmp" "$$m"; \ echo " $$m -> $$next"; \ done; \ echo "Bumped $$current -> $$next" diff --git a/README.md b/README.md index 272a707..9256075 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ installation, use the guides below. Pick your agent: - **[Gemini CLI](guides/gemini-cli.md)** - **[GitHub Copilot CLI](guides/generic.md)** - **[Goose](guides/generic.md)** -- **[Hermes Agent](guides/generic.md)** +- **[Hermes Agent](guides/hermes-agent.md)** - **[Kilo Code](guides/generic.md)** - **[Kiro](guides/generic.md)** - **[OpenCode](guides/generic.md)** diff --git a/guides/agent-install.md b/guides/agent-install.md index 49ba88f..25ea5ac 100644 --- a/guides/agent-install.md +++ b/guides/agent-install.md @@ -17,24 +17,30 @@ The default path is npm — it works in any harness with Node.js: | Claude Code | `claude plugin marketplace add officialzeroxyz/zero-plugins && claude plugin install zero@zero-plugins` | | Codex | `codex plugin marketplace add officialzeroxyz/zero-plugins && codex plugin add zero@zero-plugins` | | Gemini CLI | `gemini extensions install https://github.com/officialzeroxyz/zero-plugins` | +| Hermes Agent | `hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes --force --enable` | | Claude Chat (claude.ai) / Claude Cowork | no shell — follow [zero.xyz/install/claude.md](https://www.zero.xyz/install/claude.md) instead | | None of the above | see [Fallback installer](#fallback-installer) | The npm row installs the CLI from the public [`@zeroxyz/cli`](https://www.npmjs.com/package/@zeroxyz/cli) package (inspect it with `npm view @zeroxyz/cli`), then `zero init` integrates the skill and -hooks with every harness it detects. The Claude Code, Codex, and Gemini CLI -rows are per-harness alternatives that install through that harness's own -plugin manager: they add the zero-plugins repo as a source, then install the -Zero plugin from it via the manager's normal review-and-consent flow. The -plugin's full contents are listed below. The npm and fallback rows end by -running `zero init`. +hooks with every harness it detects. The Claude Code, Codex, Gemini CLI, and +Hermes Agent rows are per-harness alternatives that install through that +harness's own plugin manager: they add the zero-plugins repo as a source (or, +for Hermes, install straight from a subdirectory of it), then install the +Zero plugin via the manager's normal review-and-consent flow. The plugin's +full contents are listed below. The npm and fallback rows end by running +`zero init`. ## What the plugin installs -All three plugin-manager rows install the same contents, from +The plugin-manager rows install the same contents, from [`plugins/zero/`](https://github.com/officialzeroxyz/zero-plugins/tree/main/plugins/zero) -in the repo: +in the repo (Hermes Agent installs the same skill and session-start hook from +[`plugins/zero-hermes/hermes/`](https://github.com/officialzeroxyz/zero-plugins/tree/main/plugins/zero-hermes/hermes), +where they are vendored for its plugin format; instead of an auto-approve +hook — Hermes hooks can't approve — it adds a guard that blocks `zero fetch` +commands missing an explicit `--max-pay` cap, plus a `/zero` command): - **A skill** — the usage guide for the `zero` CLI ([source](https://github.com/officialzeroxyz/zero-plugins/blob/main/plugins/zero/skills/zero/SKILL.md)) diff --git a/guides/hermes-agent.md b/guides/hermes-agent.md new file mode 100644 index 0000000..6ed8854 --- /dev/null +++ b/guides/hermes-agent.md @@ -0,0 +1,36 @@ +# Zero for Hermes Agent + +How to install Zero in Hermes Agent and keep it up to date. + +Requires Hermes Agent v2026.6.19 or newer (`hermes --version`). + +## Install + +### From the terminal + +```bash +hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes --force --enable +hermes mcp add zero --url https://mcp.zero.xyz --auth oauth +``` + +The first command installs and enables the Zero plugin (hooks, the `/zero` +command, and the Zero skill) from this repository. The second adds the Zero +MCP server (optional — the plugin's `zero` CLI covers the full flow; the MCP +server helps sessions without a shell). + +Start a new Hermes session, then use `/zero` or ask Hermes: +*"Help me set up and test Zero."* It walks you through signing in. + +## Staying up to date + +- The Zero runner (the `zero` CLI) updates at the start of each session. +- To update the plugin itself, re-run the install command — with `--force` + it replaces the installed copy with the latest: + + ```bash + hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes --force --enable + ``` + + Note: `hermes plugins update zero` does not work for this plugin — Hermes + installs it from a repo subdirectory, which keeps no `.git` folder to pull + from. The `--force` reinstall above is the update path. diff --git a/plugins/zero-gemini/gemini-extension.json b/plugins/zero-gemini/gemini-extension.json index bd8e5d5..f1f35e7 100644 --- a/plugins/zero-gemini/gemini-extension.json +++ b/plugins/zero-gemini/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "zero", - "version": "1.10.0", + "version": "1.11.0", "description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.", "mcpServers": { "zero": { diff --git a/plugins/zero-hermes/README.md b/plugins/zero-hermes/README.md new file mode 100644 index 0000000..6e0cd10 --- /dev/null +++ b/plugins/zero-hermes/README.md @@ -0,0 +1,44 @@ +# zero-hermes + +Hermes Agent adapter for Zero. The installable plugin lives at +[`hermes/`](hermes/) — Hermes installs it straight from this repo's +subdirectory: + +```bash +hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes --force --enable +``` + +Requires Hermes **v2026.6.19 or newer** (the first release with subdirectory +installs). `--enable` skips the only prompt; `--force` makes the command +idempotent, so re-running it is also how the plugin is updated (a subdirectory +install keeps no `.git`, so `hermes plugins update zero` does not apply — see +the guide). + +## Layout + +- `manifest.json` — versioned release marker for this adapter (kept in + lockstep by the Makefile) +- `hermes/plugin.yaml` — Hermes plugin manifest (`name: zero`, so the plugin + installs as `~/.hermes/plugins/zero`) +- `hermes/__init__.py` — plugin registration: hooks, the `/zero` slash + command, and the plugin-namespaced `zero:zero` skill +- `hermes/mcp-zero.yaml` — config fragment for the Zero MCP server +- `hermes/hooks/ensure-runner.sh`, `hermes/skills/zero/SKILL.md` — **vendored + copies** of the shared files in [`../zero/`](../zero/), generated by + [`scripts/sync-hermes.sh`](../../scripts/sync-hermes.sh). A subdirectory + install ships only this directory, so the plugin cannot reference + `plugins/zero/` at runtime. Do not edit the vendored files directly — edit + the sources and re-run the sync script; CI + (`.github/workflows/hermes-plugin-sync.yml`) fails on drift. + +## What the plugin registers + +- `on_session_start` — runs `ensure-runner.sh` to provision the shared Zero + runner (`~/.zero/runtime/`) +- `pre_llm_call` — injects a one-line reminder that Zero is available +- `pre_tool_call` — blocks `zero fetch` terminal commands that omit an + explicit `--max-pay` cap (Hermes hooks can block but not auto-approve) +- `/zero` — slash command that routes a request through the zero skill +- `zero:zero` — the Zero skill, plugin-namespaced (explicit loads only; the + flat `~/.hermes/skills` copy that `zero init` writes remains the + discoverable one) diff --git a/plugins/zero-hermes/hermes/__init__.py b/plugins/zero-hermes/hermes/__init__.py new file mode 100644 index 0000000..b89e725 --- /dev/null +++ b/plugins/zero-hermes/hermes/__init__.py @@ -0,0 +1,103 @@ +"""Zero plugin for Hermes Agent.""" + +from __future__ import annotations + +import logging +import re +import subprocess +from pathlib import Path + +LOGGER = logging.getLogger(__name__) +PLUGIN_DIR = Path(__file__).resolve().parent + +ZERO_CONTEXT = ( + "Zero is available: a search engine and payment layer for capabilities " + "beyond Hermes' native tools. Before saying you cannot do something, or " + "telling the user to sign up for an external service, use the zero skill " + "and runner flow: search, inspect, call with an explicit max-pay cap, and " + "review successful paid runs when appropriate." +) + +ZERO_FETCH_RE = re.compile( + r"(^|[;&|]\s*|\s)(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*" + r"(?:zero|zerocli|(?:\$HOME|~)?/[^;&|'\"]*/zero)\s+fetch\b" +) + + +def _ensure_runner(**kwargs): + """Provision the shared Zero runner without breaking Hermes startup.""" + del kwargs + script = PLUGIN_DIR / "hooks" / "ensure-runner.sh" + if not script.exists(): + LOGGER.warning("Zero ensure-runner hook missing: %s", script) + return None + + try: + subprocess.run( + ["bash", str(script)], + check=False, + timeout=120, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + except Exception as exc: # pragma: no cover - defensive hook boundary + LOGGER.warning("Zero ensure-runner hook failed: %s", exc) + return None + + +def _inject_zero_context(**kwargs): + del kwargs + return {"context": ZERO_CONTEXT} + + +def _zero_guardrail(tool_name: str, args: dict, **kwargs): + """Hermes can block tools here, but it cannot auto-approve them.""" + del kwargs + if tool_name != "terminal" or not isinstance(args, dict): + return None + + command = str(args.get("command") or args.get("cmd") or "") + if ZERO_FETCH_RE.search(command) and "--max-pay" not in command: + return { + "action": "block", + "message": "Zero fetch commands must include an explicit --max-pay cap.", + } + return None + + +def _handle_zero(raw_args: str) -> str: + request = raw_args.strip() + if request: + return ( + "Use the zero skill for this request. Resolve the zero runner, run " + "`zero search`, inspect with `zero get`, and only use `zero fetch` " + f"with an explicit `--max-pay` cap. User request: {request}" + ) + return ( + "Use the zero skill. Resolve the zero runner, run `zero search`, inspect " + "with `zero get`, and only use `zero fetch` with an explicit `--max-pay` cap." + ) + + +def _register_skills(ctx): + skills_dir = PLUGIN_DIR / "skills" + if not skills_dir.exists(): + return + for child in sorted(skills_dir.iterdir()): + skill_md = child / "SKILL.md" + if child.is_dir() and skill_md.exists(): + ctx.register_skill(child.name, skill_md) + + +def register(ctx): + ctx.register_hook("on_session_start", _ensure_runner) + ctx.register_hook("pre_llm_call", _inject_zero_context) + ctx.register_hook("pre_tool_call", _zero_guardrail) + ctx.register_command( + "zero", + handler=_handle_zero, + description="Use Zero for external capabilities", + args_hint="", + ) + _register_skills(ctx) diff --git a/plugins/zero-hermes/hermes/hooks/ensure-runner.sh b/plugins/zero-hermes/hermes/hooks/ensure-runner.sh new file mode 100755 index 0000000..2ced9f0 --- /dev/null +++ b/plugins/zero-hermes/hermes/hooks/ensure-runner.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash +# +# Zero plugin — SessionStart hook. +# +# Provisions a Zero "runner" so the agent can call paid capabilities directly in its +# own environment, exports its path as $ZERO_RUNNER for the session, puts the runner +# on PATH as plain `zero` (env file for this session where supported, shell rc for +# everything else — see that section below), and tells the agent — via the +# SessionStart `additionalContext` payload — how to authenticate. Also refreshes the +# machine's Zero plugin installs once a day, in the background, through each host's +# own plugin-manager commands (see that section below). Because it both installs the +# runner and wires up PATH, this script doubles as the canonical standalone installer +# for agents with no plugin support at all. +# +# Runner model: the runner IS the published @zeroxyz/cli npm package (a single +# bundled, zero-dependency file as of 0.0.44), run on Node. We install it once per +# session into a plugin-owned runtime home and point a one-line shim at the installed +# entry, so $ZERO_RUNNER stays a single path and per-call overhead is just Node startup +# (~200ms) — not an npx resolve (~1s). The same code runs on Windows, macOS, Linux, +# and cloud sandboxes; only how we obtain Node varies: +# a. system `node` (>= min major) on PATH if present — nothing is downloaded; else +# b. download an official Node build (which bundles npm) into the runtime home, once. +# +# One Zero home (~/.zero), two deliberately namespaced areas: +# - Runtime ($HOME/.zero/runtime): the downloaded Node, the npm cache, the installed +# CLI, and the shim. Plugin-owned and disposable — safe to delete and rebuild. Kept +# in its own subdir (not loose in ~/.zero) and contained via explicit --prefix / +# npm_config_cache paths (NOT via $HOME). Shared across host plugins. +# - Auth/config (~/.zero/config.json): the SAME file the standalone CLI and +# skill.md-only agents use. The shim does NOT override $HOME, so one `auth login` +# is shared across every Zero install method on the machine (plugin, skill.md, raw +# CLI), not just plugin agents. Writes are additive (a session merges in alongside +# any existing config), so existing CLI users are not disrupted: their privateKey +# keeps signing until they opt into managed signing via `zero wallet migrate`. +# +# Auth: the agent never creates a wallet. For ephemeral/sandbox environments it mints a +# one-time code via the Zero MCP connector's authorize tool and exchanges it +# (`zero auth exchange`) for a short-lived ZERO_SESSION_TOKEN; for +# persistent environments a device-code `auth login` (persisted to ~/.zero) is +# preferred. An explicit ZERO_PRIVATE_KEY is honored for bring-your-own signing. +# +# Contract (hook mode): the ONLY thing written to stdout is a single SessionStart JSON +# object. All human/log output goes to stderr. Always exits 0 — a failed provisioning +# step degrades to a clear "unavailable" message rather than blocking the session. +# +# Install mode (--install): the standalone install path for humans and for agents with +# no plugin support — `curl -fsSL https://zero.xyz/install.sh | bash` (zero.xyz serves +# this script from main with INSTALL_MODE defaulted to 1; see the mode section below). +# Same provisioning, but: a human summary replaces the JSON object, failures exit +# non-zero so scripts/CI can tell, the host-plugin refresh sweep is skipped (no plugin +# host to refresh), and after provisioning the runner this script hands off to +# `zero init` (@zeroxyz/cli >= 1.1.0), which installs the skill + hooks — fetched from +# zero.xyz, proxies of this repo's main — into ~/.claude/ and ~/.agents/ and registers +# the hooks in ~/.claude/settings.json. Harnesses with a bespoke skills dir follow up +# with `zero init --skills-dir ` (init is idempotent — re-runs replace, never +# duplicate); `zero uninstall` reverses the whole install. + +set -euo pipefail + +# --- mode --- +# CONTRACT: zero.xyz/install.sh serves these same bytes with the next line rewritten +# to INSTALL_MODE=1, so a plain `curl -fsSL https://zero.xyz/install.sh | bash` +# installs with no flag. Keep the line exactly `INSTALL_MODE=0` — the route matches +# it verbatim and refuses to serve the script if it goes missing. +INSTALL_MODE=0 +for arg in "$@"; do + case "$arg" in + --install) INSTALL_MODE=1 ;; + esac +done + +# --- Config (override via env) --- +# Plugin-owned RUNTIME area (Node, npm cache, installed CLI, shim), namespaced under the +# user's ~/.zero so there's a single Zero home. This is NOT the auth/config file: the shim +# leaves $HOME alone, so the login in ~/.zero/config.json is shared with the standalone CLI +# and skill.md-only agents. Override the location with ZERO_PLUGINS_HOME. +ZH="${ZERO_PLUGINS_HOME:-$HOME/.zero/runtime}" + +# The published runner package and which version line to track. Default "latest"; +# pin by exporting ZERO_CLI_SPEC=0.0.44 (a concrete version skips the registry check). +CLI_PKG="@zeroxyz/cli" +CLI_SPEC="${ZERO_CLI_SPEC:-latest}" + +# Where official Node builds are fetched from, and which release line. node24 is the +# line the CLI is built/tested against. +NODE_DIST_BASE="${ZERO_NODE_DIST_BASE:-https://nodejs.org/dist}" +NODE_CHANNEL="${ZERO_NODE_CHANNEL:-latest-v24.x}" +# A system node older than this is treated as unusable -> we download instead. +NODE_MIN_MAJOR="${ZERO_NODE_MIN_MAJOR:-20}" +NODE_DIR="$ZH/node" # downloaded Node lives here (if needed) +CLI_DIR="$ZH/cli" # the installed @zeroxyz/cli +NPM_CACHE="$ZH/.npm" # contained npm/npx cache +BIN_DIR="$ZH/bin" # the shim +SHIM_PATH="$BIN_DIR/zero" +CLI_ENTRY="$CLI_DIR/node_modules/$CLI_PKG/dist/index.js" +INSTALLED_VERSION_FILE="$CLI_DIR/.installed-version" +RESOLVED_VERSION_FILE="$ZH/.cli-version" + +log() { printf '[zero] %s\n' "$*" >&2; } + +# Emit the SessionStart result. $1 is the status-specific message; JSON-escaped. +emit() { + local ctx="$1" + ctx="${ctx//\\/\\\\}" # backslashes + ctx="${ctx//\"/\\\"}" # double quotes + ctx="${ctx//$'\n'/ }" # newlines -> spaces (JSON strings can't hold raw newlines) + printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$ctx" +} + +# Report a fatal provisioning problem and stop. Hook mode emits the JSON "unavailable" +# context and exits 0 (never block a session); install mode prints the error and exits +# 1 so scripts and CI see the failure. $1 = agent-facing context, $2 = human one-liner. +fail() { + if [ "$INSTALL_MODE" = "1" ]; then + printf 'zero install failed: %s\n' "$2" >&2 + exit 1 + fi + emit "$1" + exit 0 +} + +# Append `export NAME=value` to the session env file so it persists for the session +# (Claude Code only — no-op elsewhere, e.g. Codex has no env-persistence file). +persist_env() { + if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + printf 'export %s=%q\n' "$1" "$2" >> "$CLAUDE_ENV_FILE" + fi +} + +# --- platform detect --- +OS_KIND=""; ARCH="" +case "$(uname -s)" in + Darwin) OS_KIND="macos" ;; + Linux) OS_KIND="linux" ;; + MINGW*|MSYS*|CYGWIN*) OS_KIND="win" ;; +esac +case "$(uname -m)" in + arm64|aarch64) ARCH="arm64" ;; + x86_64|amd64) ARCH="x64" ;; +esac + +node_major() { + "$1" -e 'process.stdout.write(String(process.versions.node.split(".")[0]))' 2>/dev/null || true +} + +# Resolve a usable Node runtime, echoing its path on success. Prefers a recent-enough +# system node (downloads nothing); otherwise downloads an official build (which bundles +# npm) into $NODE_DIR once. The official macOS/Linux tarball and the Windows .zip both +# include npm/npx — we need npm to install the CLI, so we never use the bare node.exe. +resolve_node() { + local sys major + sys="$(command -v node 2>/dev/null || true)" + if [ -n "$sys" ]; then + major="$(node_major "$sys")" + if [ -n "$major" ] && [ "$major" -ge "$NODE_MIN_MAJOR" ] 2>/dev/null; then + log "using system node ($sys, v$major)" + printf '%s' "$sys"; return 0 + fi + log "system node too old (v${major:-?} < $NODE_MIN_MAJOR); will download" + fi + + mkdir -p "$NODE_DIR" + local node_os="$OS_KIND"; [ "$OS_KIND" = "macos" ] && node_os="darwin" + + if [ "$OS_KIND" = "win" ]; then + # Windows: the official .zip bundles node.exe + npm + npx (the bare node.exe does + # NOT include npm, which we need). Extract with unzip if available. + local current="$NODE_DIR/current/node.exe" + if [ -x "$current" ] && "$current" --version >/dev/null 2>&1; then + log "using downloaded node ($current)"; printf '%s' "$current"; return 0 + fi + command -v unzip >/dev/null 2>&1 || { log "no unzip available to extract the Node zip on Windows"; return 1; } + local shasums artifact dir + shasums="$(curl -fsSL "$NODE_DIST_BASE/$NODE_CHANNEL/SHASUMS256.txt" 2>/dev/null || true)" + artifact="$(printf '%s\n' "$shasums" | grep -oE "node-v[0-9]+\.[0-9]+\.[0-9]+-win-${ARCH}\.zip" | head -1 || true)" + [ -n "$artifact" ] || { log "could not resolve a node $NODE_CHANNEL win-${ARCH} build"; return 1; } + dir="${artifact%.zip}" + if curl -fsSL "$NODE_DIST_BASE/$NODE_CHANNEL/$artifact" -o "$NODE_DIR/$artifact" 2>/dev/null \ + && unzip -oq "$NODE_DIR/$artifact" -d "$NODE_DIR" 2>/dev/null; then + ln -sfn "$NODE_DIR/$dir" "$NODE_DIR/current" + rm -f "$NODE_DIR/$artifact" 2>/dev/null || true + [ -x "$current" ] && "$current" --version >/dev/null 2>&1 && { log "installed node ($dir)"; printf '%s' "$current"; return 0; } + fi + rm -f "$NODE_DIR/$artifact" 2>/dev/null || true + log "node download/extract failed for win-${ARCH}" + return 1 + fi + + # macOS / Linux: official builds ship as tarballs (which include npm). Reuse an + # extracted copy if it still runs; otherwise resolve the current version from + # SHASUMS, download the .tar.gz (tar -z is universal), extract, symlink `current`. + local current="$NODE_DIR/current/bin/node" + if [ -x "$current" ] && "$current" --version >/dev/null 2>&1; then + log "using downloaded node ($current)"; printf '%s' "$current"; return 0 + fi + local shasums artifact dir + shasums="$(curl -fsSL "$NODE_DIST_BASE/$NODE_CHANNEL/SHASUMS256.txt" 2>/dev/null || true)" + artifact="$(printf '%s\n' "$shasums" | grep -oE "node-v[0-9]+\.[0-9]+\.[0-9]+-${node_os}-${ARCH}\.tar\.gz" | head -1 || true)" + [ -n "$artifact" ] || { log "could not resolve a node $NODE_CHANNEL build for ${node_os}-${ARCH}"; return 1; } + dir="${artifact%.tar.gz}" + if curl -fsSL "$NODE_DIST_BASE/$NODE_CHANNEL/$artifact" -o "$NODE_DIR/$artifact" 2>/dev/null \ + && tar -xzf "$NODE_DIR/$artifact" -C "$NODE_DIR" 2>/dev/null; then + ln -sfn "$NODE_DIR/$dir" "$NODE_DIR/current" + rm -f "$NODE_DIR/$artifact" 2>/dev/null || true + [ -x "$current" ] && "$current" --version >/dev/null 2>&1 && { log "installed node ($dir)"; printf '%s' "$current"; return 0; } + fi + rm -f "$NODE_DIR/$artifact" 2>/dev/null || true + log "node download/extract failed for ${node_os}-${ARCH}" + return 1 +} + +# Resolve CLI_SPEC to a concrete version. A purely numeric/dotted spec is used as-is. +# Otherwise (a dist-tag like "latest" or a range) we ask the registry EVERY session, so +# the runner tracks the newest published version, falling back to the last resolved +# version when offline. +resolve_cli_version() { + case "$CLI_SPEC" in + *[!0-9.]*) ;; # tag/range — resolve below + *) printf '%s' "$CLI_SPEC"; return 0 ;; # looks like X.Y.Z + esac + local v + v="$(HOME="$ZH" npm_config_cache="$NPM_CACHE" PATH="$NODE_BIN_DIR:$PATH" "$NPM_BIN" view "$CLI_PKG@$CLI_SPEC" version 2>/dev/null | tail -1 || true)" + if [ -n "$v" ]; then + printf '%s' "$v" >"$RESOLVED_VERSION_FILE" + printf '%s' "$v"; return 0 + fi + [ -s "$RESOLVED_VERSION_FILE" ] && { cat "$RESOLVED_VERSION_FILE"; return 0; } + printf '%s' "$CLI_SPEC" # last resort: let npm interpret the tag at install time +} + +# --- resolve node (+ its npm) --- +NODE_BIN="" +if [ -n "$OS_KIND" ] && [ -n "$ARCH" ]; then + NODE_BIN="$(resolve_node || true)" +fi +if [ -z "$NODE_BIN" ]; then + log "no usable Node runtime" + fail "Zero runner unavailable: no Node runtime and none could be downloaded (or no network egress). ZERO_RUNNER is unset — tell the user Zero isn't available in this environment rather than improvising." \ + "no usable Node runtime found, and none could be downloaded (check network egress)" +fi +NODE_BIN_DIR="$(dirname "$NODE_BIN")" +NPM_BIN="$NODE_BIN_DIR/npm" +[ -x "$NPM_BIN" ] || NPM_BIN="$(command -v npm 2>/dev/null || true)" + +mkdir -p "$ZH" "$BIN_DIR" "$CLI_DIR" "$NPM_CACHE" + +# --- keep the host plugin installs fresh (background, daily, best-effort) --- +# The CLI below is refreshed every session, but the *plugin layer* (skill, hooks, +# manifest) only updates when the host's marketplace machinery runs — which most users +# never enable. So once a day, refresh every Zero install on the machine through each +# host's own front-door commands; never by writing into host-owned plugin dirs: +# Claude Code - marketplace update, then plugin update (both required: plugin update +# resolves against the local catalog clone; applies next session) +# Codex - marketplace upgrade, then plugin add (upgrade re-resolves the plugin +# from the snapshot; add is idempotent and syncs the version cache) +# Gemini is deliberately NOT swept: `gemini extensions update` re-prompts for interactive +# consent whenever an update changes hooks/skills/MCP (and has no --consent flag), and a +# hook auto-answering a security prompt would be a consent bypass. Gemini freshness comes +# from its native `extensions install --auto-update` instead (see the README). +# We sweep every host CLI on PATH instead of detecting the current host: plugin installs +# are machine-global (like ~/.zero itself), so whichever host opens a session first +# freshens all of them, and absent CLIs / not-installed plugins fail quietly. The job is +# detached with all fds redirected so SessionStart never waits on it and the stdout JSON +# contract holds. The stamp is written before the attempt so a failing/offline day +# retries tomorrow rather than hammering every session. Opt out: ZERO_PLUGIN_AUTOUPDATE=0. +PLUGIN_UPDATE_STAMP="$ZH/.plugin-update-day" +if [ "$INSTALL_MODE" = "0" ] && [ "${ZERO_PLUGIN_AUTOUPDATE:-1}" != "0" ]; then + TODAY="$(date +%Y-%m-%d)" + if [ "$(cat "$PLUGIN_UPDATE_STAMP" 2>/dev/null || true)" != "$TODAY" ]; then + printf '%s' "$TODAY" >"$PLUGIN_UPDATE_STAMP" + log "refreshing host plugin installs in the background (daily; ZERO_PLUGIN_AUTOUPDATE=0 disables)" + { + command -v claude >/dev/null 2>&1 && { claude plugin marketplace update zero-plugins && claude plugin update zero@zero-plugins; } || true + command -v codex >/dev/null 2>&1 && { codex plugin marketplace upgrade zero-plugins && codex plugin add zero@zero-plugins; } || true + } /dev/null 2>&1 & + fi +fi + +# --- install / refresh the CLI (throttled) --- +VERSION="$(resolve_cli_version)" +INSTALLED="$(cat "$INSTALLED_VERSION_FILE" 2>/dev/null || true)" +if [ ! -f "$CLI_ENTRY" ] || [ "$INSTALLED" != "$VERSION" ]; then + if [ -n "$NPM_BIN" ] && HOME="$ZH" npm_config_cache="$NPM_CACHE" PATH="$NODE_BIN_DIR:$PATH" "$NPM_BIN" install \ + --prefix "$CLI_DIR" "$CLI_PKG@$VERSION" \ + --no-audit --no-fund --loglevel=error >&2 2>&1; then + printf '%s' "$VERSION" >"$INSTALLED_VERSION_FILE" + log "installed $CLI_PKG@$VERSION into $CLI_DIR" + else + log "npm install of $CLI_PKG@$VERSION failed" + fi +fi + +if [ ! -f "$CLI_ENTRY" ]; then + fail "Zero runner unavailable: $CLI_PKG@$VERSION could not be installed (likely no npm-registry egress on first run). ZERO_RUNNER is unset — tell the user Zero isn't available here rather than improvising." \ + "$CLI_PKG@$VERSION could not be installed (likely no npm-registry egress)" +fi + +# --- write the shim (regenerated each session so node/version changes take effect) --- +# Runs the installed CLI directly on the resolved Node. $HOME is deliberately NOT +# overridden, so the CLI reads/writes the user's real ~/.zero/config.json — one login +# shared with the standalone CLI and skill.md-only agents. The runtime stays contained +# in $ZH via the explicit install --prefix and npm_config_cache, not via $HOME. +# NODE_BIN_DIR is on PATH so the CLI can spawn node/subprocesses. Auth comes from the +# exchanged ZERO_SESSION_TOKEN (passed in the environment), the persisted ~/.zero +# session, or an explicit ZERO_PRIVATE_KEY (BYO). +cat >"$SHIM_PATH" </dev/null || true + +persist_env ZERO_RUNNER "$SHIM_PATH" + +# --- put `zero` on PATH (idempotent; opt out: ZERO_PATH_AUTOADD=0) --- +# Two audiences: +# - This session, on hosts with an env-persistence file (Claude Code): prepend +# $BIN_DIR via $CLAUDE_ENV_FILE so bare `zero` resolves immediately. +# - Everything else on the machine — including hosts with no env injection at all +# (e.g. Gemini CLI) — gets a PATH line appended to the user's shell rc, the same +# way the retired pre-1.0 install.sh handled ~/.zero/bin. New shells, and any +# agent launched from them, then resolve bare `zero` without $ZERO_RUNNER. +# The rc edit is keyed on the runtime-bin dir substring so it's written at most once, +# and a write failure degrades to a logged hint — it never blocks the session. +RC_PATH_ADDED="" +if [ "${ZERO_PATH_AUTOADD:-1}" != "0" ]; then + if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + persist_env PATH "$BIN_DIR:$PATH" + fi + + # Write the rc line $HOME-relative so it survives dotfile sync across machines; + # grep for the $HOME-stripped form so either spelling counts as already-present. + RC_DIR_REF="$BIN_DIR"; RC_GREP="$BIN_DIR" + case "$BIN_DIR" in + "$HOME"/*) RC_DIR_REF="\$HOME/${BIN_DIR#"$HOME"/}"; RC_GREP="${BIN_DIR#"$HOME"/}" ;; + esac + PATH_LINE="export PATH=\"$RC_DIR_REF:\$PATH\"" + + add_path_to_rc() { + local rc="$1" + if [ -f "$rc" ] && grep -qF "$RC_GREP" "$rc" 2>/dev/null; then + return 0 + fi + if printf '\n# Zero runner (added by the zero plugin SessionStart hook)\n%s\n' "$PATH_LINE" >>"$rc" 2>/dev/null; then + log "added $BIN_DIR to PATH in $rc (takes effect in new shells)" + RC_PATH_ADDED="$rc" + else + log "could not write $rc — add to PATH manually: $PATH_LINE" + fi + } + + case "$(basename "${SHELL:-}")" in + zsh) add_path_to_rc "$HOME/.zshrc" ;; + bash) + if [ -f "$HOME/.bash_profile" ]; then + add_path_to_rc "$HOME/.bash_profile" + else + add_path_to_rc "$HOME/.bashrc" + fi + ;; + *) log "unrecognized shell '${SHELL:-}' — to put zero on PATH add: $PATH_LINE" ;; + esac +fi + +INSTALLED_VERSION="$(cat "$INSTALLED_VERSION_FILE" 2>/dev/null || printf '%s' "$VERSION")" + +if [ "$INSTALL_MODE" = "0" ]; then + emit "Zero runner ready ($CLI_PKG@$INSTALLED_VERSION): invoke it as plain \`zero\` — it's on PATH (immediately on hosts that persist hook env; in new shells elsewhere). If bare \`zero\` doesn't resolve, use the absolute path $SHIM_PATH (also exported as ZERO_RUNNER where supported). Use it for the whole Zero loop (search/get/fetch/review), and follow the bundled 'zero' skill for the workflow and authentication — don't improvise auth or create a wallet." + exit 0 +fi + +# --- install mode: report the runner, then hand off to `zero init` --- +# The runner we just provisioned IS the installer for everything else: +# `zero init` fetches the skill + hooks from zero.xyz (proxies of this repo's +# main — the same content the plugin ships), writes the skill to +# ~/.claude/skills/zero/ and ~/.agents/skills/zero/, stages the hook scripts +# in ~/.zero/hooks/, and registers them in ~/.claude/settings.json. It prints +# its own step-by-step summary (ending with the `zero auth login` next step), +# and `zero uninstall` reverses it all. +if [ -n "$RC_PATH_ADDED" ]; then + PATH_STATUS="added to $RC_PATH_ADDED — open a new terminal or run: source \"$RC_PATH_ADDED\"" +elif [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then + PATH_STATUS="already on PATH" +else + PATH_STATUS="already configured in your shell rc (new shells pick it up)" +fi + +cat <&2 + exit 1 +fi +exit 0 diff --git a/plugins/zero-hermes/hermes/mcp-zero.yaml b/plugins/zero-hermes/hermes/mcp-zero.yaml new file mode 100644 index 0000000..aa48727 --- /dev/null +++ b/plugins/zero-hermes/hermes/mcp-zero.yaml @@ -0,0 +1,7 @@ +mcp_servers: + zero: + url: "https://mcp.zero.xyz" + auth: oauth + enabled: true + timeout: 120 + connect_timeout: 60 diff --git a/plugins/zero-hermes/hermes/plugin.yaml b/plugins/zero-hermes/hermes/plugin.yaml new file mode 100644 index 0000000..aa4e126 --- /dev/null +++ b/plugins/zero-hermes/hermes/plugin.yaml @@ -0,0 +1,11 @@ +name: zero +version: 1.11.0 +description: Zero capability search and payment layer for Hermes Agent +provides_hooks: + - on_session_start + - pre_llm_call + - pre_tool_call +provides_commands: + - zero +provides_skills: + - zero diff --git a/plugins/zero-hermes/hermes/skills/zero/SKILL.md b/plugins/zero-hermes/hermes/skills/zero/SKILL.md new file mode 100644 index 0000000..4619b6c --- /dev/null +++ b/plugins/zero-hermes/hermes/skills/zero/SKILL.md @@ -0,0 +1,403 @@ +--- +name: zero +description: Use Zero for capabilities beyond Hermes. +author: Zero +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [zero, capability-search, mcp, x402] + category: automation +--- + +# zero + +Zero is a search engine and payment layer for AI agents: discover external paid capabilities +(x402 / MPP services), call them, and pay per use — no per-service signup. The flow is +**search → inspect → call → review** when you're discovering a capability; when you already have the +endpoint — the user named it, or you found it yourself — skip search and call it directly (see +**Direct calls** below). + +**When to use it:** as the fallback for anything genuinely beyond your native abilities — before +telling the user "I can't do that," run a `zero search`. **When NOT to use it:** for things you +already handle yourself — writing code, answers from your own knowledge, local files, shell +commands, math. Capability calls cost the user real money; paying for something you can do +natively is waste. If you can do it yourself, just do it. + +Two surfaces give you Zero: + +- **The runner** — the `zero` CLI, provisioned by the plugin/extension for this session or + installed standalone by the user (see **Resolving `zero`**). This is your primary tool: it runs + the whole loop, handles 402 payment (including cross-chain), streams binary output, and enforces + spend caps. +- **The MCP connector** (`https://mcp.zero.xyz`) — the Zero connector tool surface. Its job is + **authentication and funding**, not the loop. In ephemeral/sandbox environments it is also the + *only* way to authenticate the runner (see below). + +## Resolving `zero` + +Every example below invokes the runner as plain `zero`, and that is also what you should type +in real commands whenever it resolves: users read your commands, and `zero search …` reads like +a normal CLI where `"$ZERO_RUNNER" search …` reads like machinery. The name is generic, though, +so resolve it once, before your first call — don't trust `$PATH` blindly. Take the first tier +that resolves to a working executable, then use the same spelling everywhere `zero` appears +below: + +1. **`zero` on `$PATH`** — preferred. Either the provisioned runner (the SessionStart hook + prepends its directory to PATH: immediately on hosts that persist hook env vars, in new + shells elsewhere) or a standalone CLI install (`npm install -g @zeroxyz/cli`). Trust it + without further checks when `command -v zero` matches `$ZERO_RUNNER` or points into + `$HOME/.zero/runtime/bin`; any other path counts only if `zero --help` prints the Zero CLI + header (`Zero CLI — Search engine for AI agents`). Anything else → fall through. +2. **`$ZERO_RUNNER`** — the runner's absolute path, exported by the SessionStart hook on hosts + that persist hook env vars (Claude Code, Codex). The fallback spelling when bare `zero` + doesn't resolve yet or failed the check above. +3. **`$HOME/.zero/runtime/bin/zero`** — the provisioned runner's well-known path, for hosts + that don't persist hook env vars (e.g. Gemini CLI); the SessionStart hook reports it. +4. **`npx -y @zeroxyz/cli@latest`** — ephemeral/sandbox environments only, where nothing is + provisioned or installed (see **Ephemeral / sandbox** below). + +```bash +ZERO="$(command -v zero || true)" # tier 1 — verify per the rules above +[ -n "$ZERO" ] || ZERO="${ZERO_RUNNER:-}" +[ -x "$ZERO" ] || ZERO="$HOME/.zero/runtime/bin/zero" +``` + +When tier 1 wins, invoke it as plain `zero`; on the lower tiers use the absolute path (which +also survives shells that don't persist variables between commands). + +If no tier resolves in a persistent environment, tell the user Zero isn't available here — +don't install the CLI yourself. In an ephemeral sandbox, fall through to `npx`. Never generate +a private-key wallet yourself either way (managed wallets come from auth — see below). + +## The runner + +The runner is the published `@zeroxyz/cli`. Under the plugin/extension it's installed once per +session into a shared, plugin-owned home and reported by its absolute path (also exported as +`$ZERO_RUNNER` on hosts that persist hook env vars; default path `~/.zero/runtime/bin/zero`); a +standalone `npm install -g @zeroxyz/cli` serves the same role. Either way you do **not** install, +update, or configure it yourself, and the runner needs no wallet setup. Identity comes from a +session (below); signing is managed server-side. + +**Prefer the runner for every step of the loop, even when MCP search/get/fetch tools are also +available.** The runner is the complete, auditable path: it pays 402 challenges automatically, +applies `--max-pay` caps, separates body from progress on stdout/stderr, and writes binary +responses to disk. Use the connector only for what the runner can't do itself: authenticating in a +sandbox, and funding. + +## Authentication + +Before touching any auth command, decide **who the account is for**: + +- **A human is present** (or asked you to sign *them* in) → `zero auth login`. It's the device + flow below and creates their account on first sign-in; `zero auth register` is an alias for + the same command, not a separate signup. +- **Fully autonomous — no human in the loop** → `zero auth agent register`. Anonymous account + plus a managed wallet, no browser, no one to hand a URL to. +- **Never** use `zero auth agent register` when a human is present — it mints an account owned + by *no one* (a human can only take it over later via the claim flow). If there's a human, + `zero auth login` already creates their account. + +Beyond that, the environment picks the mechanics. + +### Persistent — the user's own computer (not a sandbox or cloud runner) + +Authenticate the runner with the **device-code login**. It's non-interactive and agent-friendly: +you start it, show the user a URL, then run the finish step — which **polls on its own** until they +authorize. No browser is opened on the machine running the agent, and you do **not** wait for the +user to tell you they're done — the finish command blocks until it knows. + +```bash +# 1. Start: prints a URL + user code and exits immediately (no waiting, no browser). +zero auth login --start --json +# → {"deviceCode":"…","userCode":"WXYZ-1234","verificationUri":"https://…", +# "url":"https://…?code=WXYZ-1234","pollInterval":5,"expiresAt":…} + +# 2. Show the user the "url" (and the userCode) and ask them to authorize it in their browser. + +# 3. Immediately run finish. It BLOCKS and polls (~every 2s) until the user authorizes, then +# persists the session. Run it right after step 2 — do NOT pause to ask "are you done yet?"; +# the command returning is your signal. +zero auth login --finish --json +# → {"status":"ok","user":{"id":"…","email":"…"}} once authorized +# → {"status":"expired"} if the ~10 min code TTL lapses first +``` + +Treat the finish command's return as the source of truth: a successful exit means the session is +already persisted; `{"status":"expired"}` means the code lapsed — start over from step 1. Never +block the conversation polling by hand or waiting on the user to confirm; `--finish` is the poll. + +The session is saved to the shared `~/.zero/config.json`, so authenticating once here also signs +you in everywhere Zero is used on this machine — the standalone `zero` CLI and your other agents +all share the one login. + +Check identity any time with `zero auth whoami`. + +### Autonomous — no human in the loop (`zero auth agent register`) + +When there is genuinely no human to send to a browser, register an anonymous agent account: + +```bash +zero auth agent register --json +# → {"status":"ok","registrationId":"…","userId":"…","walletAddress":"0x…", +# "claimTokenExpires":"…"} +``` + +One command, no browser, no waiting. What you get: + +- A signed-in session, persisted to `~/.zero/config.json` like any other login. +- A **managed wallet, created at signup** — you can search, get, fetch, and pay immediately. + It starts empty: fund it with `zero wallet fund` (`--no-open` prints the funding URL instead + of opening a browser; the link is one-time use). +- A **claim token**, saved in `~/.zero/config.json` — a human's future path to owning this + account. It expires (the deadline is printed at registration and stored as + `claimTokenExpires` in the config), and an unclaimed account is cleaned up after the + registration expires — so hand the account to a human (below) before then if the work + should outlive it. + +If it errors with "Already signed in", there's an existing session — don't stack a fresh +anonymous account on top of it. + +### Claim handoff — `zero auth agent claim ` + +When a human should own an agent-registered account, link it to them. The ceremony is the +**reverse of the device flow**: the hosted claim page shows the *human* a pairing code, they +read it back to you, and *you* complete with the code. + +```bash +# 1. Start: prints the hosted claim URL. +zero auth agent claim human@example.com + +# 2. Send the human the URL. They sign in as that email and the page shows THEM a pairing code. + +# 3. They read the code back to you. On a TTY the command prompts for it inline; in +# non-interactive runs, complete with a second invocation: +zero auth agent claim human@example.com --code +``` + +`--code` completes the attempt already in flight — re-running *without* `--code` mints a fresh +link and invalidates the code the human is holding. After the claim, the account belongs to the +human, every pre-claim session is revoked, and the CLI rotates onto fresh credentials +automatically — nothing to re-run. If the start step fails, the claim token has likely expired +(check `claimTokenExpires` in `~/.zero/config.json`). + +### Ephemeral / sandbox — cloud, CI, web/mobile sandbox runners + +A device-code login won't work — the environment can't hold a persisted session. Run the published +`@zeroxyz/cli` directly and authenticate via the **MCP connector's `authorize` tool**. + +**Code execution is required.** This whole path runs the CLI in a shell; if the environment can't +execute commands, stop and have the user enable it. + +**Egress (reactive, not a gate).** `zero fetch` calls each capability's own URL on arbitrary +domains, so the environment needs unrestricted outbound access — not just the npm registry and +`*.zero.xyz`. A Zero-only allowlist passes search/get and then breaks every fetch. Don't verify +this up front. If a fetch hangs or fails with a DNS/connection error, restricted egress is the +first thing to suspect: have the user select "All domains" (or the equivalent unrestricted-egress +option). Reaching `*.zero.xyz` alone doesn't prove arbitrary-domain egress. + +1. **Get the runner — install once, don't `npx` per call.** Per-call + `npx -y @zeroxyz/cli@latest` re-resolves the package every time (~2–3s each, even warm) and + prints npm notices to stderr. If the sandbox filesystem persists across commands — most do, + even when each command runs in a fresh *shell* — install once and call the bare binary: + + ```bash + npm install -g @zeroxyz/cli@latest # ~1s cached; puts `zero` on PATH + zero --help # ~0.9s/call thereafter + ``` + + Only if the filesystem is reset between commands (no install survives) fall back to + `npx -y @zeroxyz/cli@latest` everywhere `zero` appears below. +2. **Authorize.** If the connector (`https://mcp.zero.xyz`) isn't available as a tool yet, walk the + user through adding it and the one-time consent. Then call its **`authorize`** tool to get a + short-lived authorization `code`. +3. **Exchange the code for a session token — without printing it.** The code from `authorize` is + one-time; the reusable credential is the session token that `zero auth exchange` returns. The + command writes the bare token to stdout precisely so you can capture it straight to disk. Never + run it bare, `echo`/`cat` the token, or paste it into the conversation — the token must not land + in the transcript. The token file lives on disk, so — like the install above — it survives a + fresh shell; the axis that matters is filesystem persistence, not env persistence. + + ```bash + # Typical harness: fresh shell per command (env vars don't carry over), persistent filesystem. + # Write the token to an owner-only file once, then load it per call: + (umask 077; zero auth exchange > /tmp/zero-session-token) + ZERO_SESSION_TOKEN="$(cat /tmp/zero-session-token)" zero search "…" + ``` + + Every CLI call picks `ZERO_SESSION_TOKEN` up from the environment. (`auth exchange --json` emits + `{token, expiresAt}` instead, if you need the expiry.) +4. **Re-mint when it expires.** The token is short-lived and has no refresh path. If calls start + failing with an auth error mid-task, call `authorize` again, re-run `auth exchange`, and + re-capture `ZERO_SESSION_TOKEN` the same way. +5. **Reviewing across a fresh shell.** The `runId` on `fetch`'s stderr is gone by the next command + in a fresh-shell harness — so capture it from the `--json` envelope at call time (see **Output + handling**) and review by runId rather than re-deriving the slug (see **Reviews — what to + write**). + +### Bring-your-own signing + +If the user supplies their own wallet key, set `ZERO_PRIVATE_KEY=0x…` in the environment. It takes +precedence for signing and works alongside either identity path above. Only use a key the user +explicitly provides; never generate one. + +### Funding + +Funding is managed server-side. If a call fails for insufficient balance, point the user to +https://www.zero.xyz/profile to fund their Zero account. On an agent-registered account there +is no signed-in human profile — use `zero wallet fund --no-open` and relay the one-time funding +URL instead. + +## Direct calls + +Zero works on any endpoint, not just indexed ones. Whenever you already have a specific URL to call +— the user named it, or you discovered it yourself (e.g. a storefront or API you found while +browsing) — call it directly: `zero fetch `. Being absent from Zero's index is no reason to +refuse it or to swap in a different, indexed capability instead. Search is for when you need to +*find* a capability; don't insist on it when you already know what to call. + +A `--capability` value (token, slug, or uid) comes from `zero search` or a capability's page, so you +won't have one for a URL you reached this way — omit it and just pass the URL. The server matches the +URL to a capability on its own for attribution when it recognizes it; if it doesn't, the call still +runs, it just isn't recorded as a reviewable run. + +## The loop + +1. **Search** — `zero search "weather forecast"`. Always re-search; capabilities, prices, + and rankings churn. Never reuse URLs/schemas/prices from memory or earlier in the conversation. + Each result includes a short **attribution token** (`token` field, format `z_xxx.N` where `N` is + the 1-based position). Use this token — not the position number — in subsequent steps; it encodes + the search context so the run is tied back to the originating search for attribution. +2. **Inspect** — `zero get --formatted` (e.g. `zero get z_Ab12cd.1`) prints a human + summary plus a copy-pasteable `Try it:` line. Plain `zero get ` returns full JSON + (URL, method, `bodySchema`, examples, pricing). You can also pass a slug or uid. If `bodySchema` + is `null`, skip that result — don't invent field names. +3. **Call** — `zero fetch --capability [-d ''] [-H 'k:v'] [--max-pay 0.50]`. + Pass `--capability ` so the run is recorded and attributed to the search. 402 responses + are paid automatically (x402 + MPP, including cross-chain bridging from Base to Tempo). +4. **Review** — `zero review --success --accuracy N --value N --reliability N --content ""`. + `--success` (or `--no-success` when the capability failed) is **required** — the command errors + without one. The `runId` is printed to stderr (or in the `--json` envelope). Always review after + a paid call. + +## Request shape + +Read `bodySchema` from `zero get` first. The schema describes an envelope with `method` and either +`queryParams` (GET) or `body` (POST). Translate it into a real HTTP call — do **not** send the +envelope as the body. + +GET — encode `queryParams` as query string: + +```bash +zero fetch "https://api.example.com/locate?ip=8.8.8.8" +``` + +POST — send `input.body` as JSON: + +```bash +zero fetch https://api.example.com/translate \ + -d '{"text":"hello","to":"es"}' \ + -H "Content-Type:application/json" +``` + +## `zero fetch` flags + +| Flag | Use | +|---|---| +| `-X ` | Force HTTP method. Defaults to POST when `-d` is set, else GET. | +| `-d ` | Inline JSON, `@./file`, or `@-`/`--data-stdin`. Implies POST + sets `Content-Type: application/json` if you didn't pass `-H`. | +| `-H 'k:v'` | Repeatable. Caller-provided auth/API keys the capability requires. | +| `--max-pay ` | Hard spend cap per call. Set this before unfamiliar or per-call-priced capabilities. | +| `--timeout ` | Per-request timeout (default 60), applied to each HTTP leg — probe and paid retry — not as a wall-clock deadline. Raise it up front for slow capabilities (image/video/audio often need `--timeout 300`) so the call doesn't die at 60s after payment. | +| `--json` | `{runId, ok, status, latencyMs, payment, body, bodyRaw}` envelope on stdout. Use `ok`, not `status`, for success. `body` is parsed JSON; `bodyRaw` is the literal text. | +| `--raw-body` | With `--json`, keep `body` as the raw string. | +| `--capability ` | Attribution token (`z_xxx.N` from search), slug, or uid — records the run and attributes it to the search. Pass it whenever you have one: the token from search results, or a slug/uid you already hold. You won't have any of these for a URL you reached without searching (see **Direct calls**) — omit it and the server attributes the URL itself when it can. | + +`-d` rejects bodies over 10 MB. Inline `-d ''` past ~1 MB hits shell arg limits — use +`-d @file` or `--data-stdin`. + +## Output handling + +`zero fetch` separates streams: +- **stdout** — response body only (or `--json` envelope, or binary bytes — redirect with + `> out.png` for images/audio/PDF). +- **stderr** — progress, payment info, the `Run ID:` line, warnings. + +```bash +zero fetch "" | jq . # body on stdout +zero fetch --json "" | jq 'select(.ok)' # programmatic +zero fetch "" > out.png # binary +``` + +Capture the `runId` from the `--json` envelope (`{runId, ok, …}`) rather than scraping it from +stderr — it's the handle `zero review` needs, and a prior call's stderr may be gone by the time you +review (especially in a fresh-shell harness). + +## Reviews — what to write + +`--content` is free-form, optional, and **strongly encouraged when you have a specific +observation.** It lands on the capability's public page on zero.xyz, so it doubles as signal for +the next agent and copy for human buyers. + +Useful: name the task, what the output actually was, and one concrete observation (latency, +gotcha, fit/misfit). + +> "Generated the requested gremlin-on-couch image faithfully in ~140ms. Schema straightforward, +> output URL loaded cleanly. At $0.003 the price-to-quality ratio is excellent." + +> "FLUX Schnell returned HTTP 500 — paid 0.003 USDC via MPP but got no image." (pair with +> `--no-success`) + +Skip `--content` rather than write filler ("Worked great", "Fast"). Submit numeric ratings alone +if you have nothing specific. + +Every review needs an outcome flag or the command errors: `--success` when the call delivered, +`--no-success --content ""` when the capability failed. Skip review only if the failure +was a CLI-internal bug (e.g. `No client registered for x402 version: N`) — file `zero bug-report` +instead. + +Review by `runId` (from the `fetch --json` envelope). `zero review --capability ` can +auto-resolve to your most recent unreviewed run, but only with the *exact* recorded slug — which is +host-prefixed and hash-suffixed (e.g. `image-withzero-xyz-…-f422560b`), not guessable. Lost the +runId? `zero runs --unreviewed` lists every pending run with its slug. + +## Gotchas + +Quick pre-flight, each detailed above: re-search every time; `zero get` before every `zero fetch`; +encode GET `queryParams` as a query string (don't POST the envelope); skip `bodySchema: null` rather +than guess fields; check `ok`, not `status`; set `--max-pay` on anything unfamiliar; raise +`--timeout` for slow image/video/audio so the call doesn't die after payment; every `zero review` +needs `--success`/`--no-success`; pass `--capability ` whenever you have one. + +- **`--capability` drives review attribution** — after a search, pass the `z_xxx.N` token so the run + ties back to it; that keeps the capability's reliability signal fresh and lets you review it. If + you already hold a slug/uid but didn't search, pass that. For a URL you reached without either (see + **Direct calls**) you have nothing to pass — omit it; the server attributes the URL when it can, + and only a URL it can't resolve ends up as an unreviewable run. +- **Before ending a multi-call task, run `zero runs --unreviewed`** and review anything you missed. +- **Zero reminder injected twice per prompt?** A plugin install and a standalone install + (`zero init`) are coexisting; the harness may also warn the user about a shadowed Zero + skill or hook. Harmless — don't fix it by deleting files. If the user wants the duplicate + gone, `zero uninstall` removes the standalone copy — machine-wide, for every app reading + the shared `~/.claude`/`~/.agents` directories — so offer it, don't run it unprompted. + +## End-to-end + +```bash +zero search "sentiment analysis" +# Each result shows a token (z_xxx.N) — use it as the capability reference +zero get z_Ab12cd.1 --formatted +zero fetch https://nlp-api.example.com/sentiment \ + --capability z_Ab12cd.1 \ + -d '{"text":"Zero is great"}' \ + -H "Content-Type:application/json" +# Run ID printed on stderr (also in --json envelope as .runId) +zero review abc123 --success --accuracy 5 --value 4 --reliability 5 \ + --content "Classified a 200-char product-review snippet positive in ~180ms; matched manual read. Clean schema, no auth." +``` + +## Reporting Zero platform bugs + +`zero bug-report ""` — only when the user explicitly asks ("file a bug"). For Zero-side +issues (bad ranking, wrong indexed URL, billing off, CLI misbehavior). **Never** substitute it for +`zero review` — capability quality always belongs in a review. diff --git a/plugins/zero-hermes/manifest.json b/plugins/zero-hermes/manifest.json new file mode 100644 index 0000000..ed7119e --- /dev/null +++ b/plugins/zero-hermes/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "zero-hermes", + "version": "1.11.0", + "description": "Hermes Agent adapter for Zero: plugin hooks, slash command, Hermes skill package, and MCP config fragment.", + "homepage": "https://zero.xyz", + "repository": "https://github.com/officialzeroxyz/zero-plugins", + "license": "MIT" +} diff --git a/plugins/zero/.claude-plugin/plugin.json b/plugins/zero/.claude-plugin/plugin.json index b3e06b9..985bfd6 100644 --- a/plugins/zero/.claude-plugin/plugin.json +++ b/plugins/zero/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "zero", - "version": "1.10.0", + "version": "1.11.0", "description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.", "author": { "name": "Zero", "url": "https://zero.xyz" }, "homepage": "https://zero.xyz", diff --git a/plugins/zero/.codex-plugin/plugin.json b/plugins/zero/.codex-plugin/plugin.json index 88d6384..1dd3684 100644 --- a/plugins/zero/.codex-plugin/plugin.json +++ b/plugins/zero/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "zero", - "version": "1.10.0", + "version": "1.11.0", "description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill.", "author": { "name": "Zero", "url": "https://zero.xyz" }, "homepage": "https://zero.xyz", diff --git a/plugins/zero/.factory-plugin/plugin.json b/plugins/zero/.factory-plugin/plugin.json index b3e06b9..985bfd6 100644 --- a/plugins/zero/.factory-plugin/plugin.json +++ b/plugins/zero/.factory-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "zero", - "version": "1.10.0", + "version": "1.11.0", "description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.", "author": { "name": "Zero", "url": "https://zero.xyz" }, "homepage": "https://zero.xyz", diff --git a/plugins/zero/agents.json b/plugins/zero/agents.json index 10af7a0..d45c10f 100644 --- a/plugins/zero/agents.json +++ b/plugins/zero/agents.json @@ -271,7 +271,14 @@ { "id": "hermes", "name": "Hermes Agent", + "minCli": "1.14.0", "detect": { "exe": "hermes", "dir": ".hermes" }, + "install": { + "executable": "hermes", + "steps": [ + { "label": "install plugin", "command": "hermes", "args": ["plugins", "install", "officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes", "--force", "--enable"] } + ] + }, "standalone": { "skills": [".hermes/skills"] } diff --git a/scripts/sync-hermes.sh b/scripts/sync-hermes.sh new file mode 100755 index 0000000..3ca66ac --- /dev/null +++ b/scripts/sync-hermes.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Vendor the shared Zero files into the Hermes plugin directory. +# +# Hermes installs the plugin straight from this repo's subdirectory +# (`hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes`), +# and a subdirectory install ships ONLY what is checked in under that path — +# the installed plugin cannot reach sibling directories like plugins/zero/. +# So the shared hook script and skill are vendored (committed) into the plugin +# directory by this script, and CI (hermes-plugin-sync.yml) fails any PR where +# the vendored copies drift from their sources. +# +# Usage: +# scripts/sync-hermes.sh # rewrite the vendored files in place +# scripts/sync-hermes.sh --check # exit 1 if the vendored files are stale +# +# Sources: +# plugins/zero/hooks/ensure-runner.sh -> plugins/zero-hermes/hermes/hooks/ensure-runner.sh (verbatim) +# plugins/zero/skills/zero/SKILL.md -> plugins/zero-hermes/hermes/skills/zero/SKILL.md (Hermes frontmatter, shared body) + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SHARED="$ROOT/plugins/zero" +PLUGIN="$ROOT/plugins/zero-hermes/hermes" + +CHECK=0 +[ "${1:-}" = "--check" ] && CHECK=1 + +for f in "$SHARED/hooks/ensure-runner.sh" "$SHARED/skills/zero/SKILL.md"; do + [ -f "$f" ] || { echo "sync-hermes: missing source: $f" >&2; exit 1; } +done + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT +mkdir -p "$STAGE/hooks" "$STAGE/skills/zero" + +cp "$SHARED/hooks/ensure-runner.sh" "$STAGE/hooks/ensure-runner.sh" + +# Hermes skills use their own frontmatter shape; keep the shared skill body. +cat > "$STAGE/skills/zero/SKILL.md" <<'HEADER' +--- +name: zero +description: Use Zero for capabilities beyond Hermes. +author: Zero +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [zero, capability-search, mcp, x402] + category: automation +--- +HEADER +awk ' + NR == 1 && $0 == "---" { in_frontmatter = 1; next } + in_frontmatter && $0 == "---" { in_frontmatter = 0; next } + !in_frontmatter { print } +' "$SHARED/skills/zero/SKILL.md" >> "$STAGE/skills/zero/SKILL.md" + +if [ "$CHECK" = "1" ]; then + status=0 + for rel in hooks/ensure-runner.sh skills/zero/SKILL.md; do + if ! diff -u "$PLUGIN/$rel" "$STAGE/$rel" >&2 2>/dev/null; then + echo "sync-hermes: $rel is out of sync — run scripts/sync-hermes.sh" >&2 + status=1 + fi + done + exit "$status" +fi + +mkdir -p "$PLUGIN/hooks" "$PLUGIN/skills/zero" +cp "$STAGE/hooks/ensure-runner.sh" "$PLUGIN/hooks/ensure-runner.sh" +cp "$STAGE/skills/zero/SKILL.md" "$PLUGIN/skills/zero/SKILL.md" +chmod +x "$PLUGIN/hooks/ensure-runner.sh" +echo "sync-hermes: vendored files refreshed" From 102a81c59cfb3df18eb53bb8015a78fbc4ac7db7 Mon Sep 17 00:00:00 2001 From: Spencer Charest Date: Mon, 20 Jul 2026 08:15:33 -0700 Subject: [PATCH 2/2] docs(guides): recommend CLI auth for Hermes; MCP only for ephemeral environments Co-Authored-By: Claude Fable 5 --- guides/hermes-agent.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/guides/hermes-agent.md b/guides/hermes-agent.md index 6ed8854..b96762d 100644 --- a/guides/hermes-agent.md +++ b/guides/hermes-agent.md @@ -10,16 +10,22 @@ Requires Hermes Agent v2026.6.19 or newer (`hermes --version`). ```bash hermes plugins install officialzeroxyz/zero-plugins/plugins/zero-hermes/hermes --force --enable -hermes mcp add zero --url https://mcp.zero.xyz --auth oauth ``` -The first command installs and enables the Zero plugin (hooks, the `/zero` -command, and the Zero skill) from this repository. The second adds the Zero -MCP server (optional — the plugin's `zero` CLI covers the full flow; the MCP -server helps sessions without a shell). +This installs and enables the Zero plugin (hooks, the `/zero` command, and +the Zero skill) from this repository. Start a new Hermes session, then use `/zero` or ask Hermes: -*"Help me set up and test Zero."* It walks you through signing in. +*"Help me set up and test Zero."* It walks you through signing in with +`zero auth login` — a device-code login persisted to `~/.zero`, shared with +every other Zero install on the machine. + +If you run Hermes in an ephemeral environment where a session can't persist +to disk, add the Zero MCP server instead and authorize through it: + +```bash +hermes mcp add zero --url https://mcp.zero.xyz --auth oauth +``` ## Staying up to date