* fix+release(v0.4.1): D74 post-Phase-4 hotfix batch — maintainer-review findings Patch release fixing 5 issues caught by maintainer post-v0.4.0 independent review (main / v0.4.0 / commitee4d945). All five are real runtime bugs the per-D-day fresh-context opus reviewers missed because they checked spec text instead of runtime contracts (default auth.allow_anonymous: false, real /health payload shape, real /cache/stats payload shape, real /v0/management/dashboard-data payload shape). Phase 4 process learning: every implementation D-day MUST include at least one test that boots the server with default production config and exercises the new feature end-to-end. D74 Suite 36 pins the wire- contract shapes so a future D-day refactor can't silently re-break the CLI / plugin / docs. ## P1-1 — olp doctor false-negative on auth-required /health lib/doctor.mjs: buildBuiltinChecks() accepts opts.authHeaders and passes to httpGet for both server.running + server.version probes. The server.running check now distinguishes 401/403 ("server up, bearer token missing or invalid — set OLP_API_KEY") from "server unreachable" so the kind discriminator routes to a clean fix-auth path instead of fix_server when operator just forgot to export the env var. bin/olp.mjs cmdDoctor: threads authHeaders() through to runDoctor. ## P1-2 — olp-connect token validation + shell-quoting bin/olp-connect: validate_olp_token() enforces ^olp_[A-Za-z0-9_-]{43}$ (per ADR 0007 § 3 token format) at THREE input sites: --key arg, /health.anonymousKey server-advertised consumption, interactive prompt fallback. shell_quote() POSIX-single-quote-wraps with embedded-quote escape per: foo'bar → 'foo'\''bar' Applied to all rc-file writes + dry-run output. systemd environment.d/olp.conf write additionally rejects embedded newlines. Hostile or malformed keys can no longer persist as shell startup injection. ## P2-3 — olp usage + olp cache human formatter wire-contract fix bin/olp.mjs cmdUsage previously read body.usage_24h.requests / body.providers / body.top_fallback_chains — none of which exist in the real server payload (server.mjs:2027 + lib/audit-query.mjs). Users saw "requests: ?" + missing per-provider quota + missing top-chains. Now reads body.window_24h.request_count / body.cache_hit_24h.hit_rate / body.quota / body.top_fallback_chains_24h. bin/olp.mjs cmdCache previously read body.entries / body.bytes / body.maxBytes (OCP-era field names). Real CacheStore.stats() returns {hits, misses, size, inflightCount}. Now reads body.size / body.inflightCount + computes hit rate from hits/(hits+misses). ## P2-4 — olp-plugin/ fmtHealth iterates providers.status olp-plugin/index.js: previously walked Object.entries(body.providers) which surfaced `enabled` / `available` / `status` as pseudo-providers (chat showed "🟢 status" instead of "🟢 anthropic"). Now extracts the real provider map from body.providers.status, renders enabled/available counts in a header line, lists per-provider names with activeSpawns when present. Falls back to flat body.providers.* for older OCP shape (backwards compat). ## P3-5 — stale v0.3.0-era doc strings updated README.md: header status line + Implementation Status § now reflect v0.4.0 shipped + Phase 5 open. Known-limitations Phase 3 line moved out of "pending v0.3.0" state; new Phase 4 line added with full deliverable list. server.mjs: startup banner no longer hardcodes "Phase 1 in progress" (now just lists version + provider count). Banner derives state from VERSION so future Phase boundaries don't need touch-ups here. ## Test count 696 (v0.4.0) → 704 (v0.4.1). +8 D74 regression tests in Suite 36: - 36a: runDoctor accepts authHeaders + threads to checks - 36b: server.running distinguishes 401 (auth) from "server down" - 36c: olp-connect rejects malformed --key (validator fires before rc write) - 36d: olp-connect accepts properly-formed olp_ token - 36e: CacheStore.stats() shape pin + cmdCache source pin - 36f: dashboard-data payload shape pin + cmdUsage source pin - 36g: olp-plugin fmtHealth iterates providers.status not providers.* - 36h: server.mjs banner doesn't hardcode stale phase ## Authority - Maintainer independent review of main / v0.4.0 / commitee4d945(2026-05-26 session — 5 findings P1×2 + P2×2 + P3×1) - Iron Rule 第二律 (evidence over "should work") — runtime smoke against default production config now mandatory per Suite 36 pattern - CLAUDE.md release_kit.phase_rolling_mode cross-Phase discipline ("hotfix to a shipped Phase N deliverable → bump patch, tag, release before next push") - ADR 0007 § 3 (token format ^olp_[A-Za-z0-9_-]{43}$) — D74 P1-2 validator authority Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: Suite 36 paths use import.meta.dirname for CI portability (was hardcoded /Users/taodeng/olp/) --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
olp-plugin
OpenClaw gateway plugin that exposes a /olp slash command on Telegram and
Discord, with subcommand parity to the local olp CLI (bin/olp.mjs) minus
mutating operations.
Authority: ADR 0010 § Phase 4 D71-D73.
Status
✅ Shipped at v0.4.0 (read-only subset of olp CLI).
What you can do from chat
| Slash command | Maps to | Tier |
|---|---|---|
/olp status |
GET /v0/management/status |
owner |
/olp health |
GET /health |
public |
/olp usage |
GET /v0/management/dashboard-data |
owner |
/olp models |
GET /v1/models |
public |
/olp cache |
GET /cache/stats |
owner |
/olp providers |
local registry view | public |
/olp chain show [model] |
local chain view (empty unless wired) | public |
/olp doctor |
informational only (HTTP doctor endpoint not yet shipped) | — |
/olp help |
usage text | — |
What you can NOT do from chat (by design)
The following olp CLI subcommands are deliberately not ported to the
chat surface, because Telegram + Discord are shared / persistent message
streams and key material or raw audit logs should not be flowing across
them:
olp keys keygen— key material would land in chat historyolp keys revoke— accidental misclick could lock out clientsolp restart— a misclick should not cycle the proxyolp logs— audit content may carry PII
Use SSH to the host running OLP and the local olp CLI for those.
Install
The plugin is shipped inside the OLP repo at olp-plugin/. Two install paths:
Option A — OpenClaw CLI
openclaw plugins install /path/to/olp/olp-plugin/
Option B — symlink
mkdir -p ~/.openclaw/extensions/
ln -s /path/to/olp/olp-plugin/ ~/.openclaw/extensions/olp
Either path makes the plugin discoverable; restart the gateway to pick it up:
openclaw gateway restart
Configure
Edit ~/.openclaw/openclaw.json and add a config block for the olp plugin:
{
"plugins": {
"olp": {
"proxyUrl": "http://127.0.0.1:4567",
"apiKey": "olp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
}
}
-
proxyUrl— full URL of the OLP proxy. Defaulthttp://127.0.0.1:4567(OLP's default since v0.4.0 / D60). Overridable viaOLP_PROXY_URLorOLP_PORTenv if you run the gateway under launchd / systemd with custom env. -
apiKey— owner-tier OLP API key. Required for the subcommands markedownerin the table above. Create one with:# On the OLP host, NOT in chat: npx olp-keys keygen --owner --name=openclaw-bot # Capture the plaintext token from the output — it is printed exactly once.Use a dedicated bot key (the
--name=openclaw-botexample above) so you cannpx olp-keys revoke --id=<id>later without affecting the maintainer's personal key.
Use
In Telegram or Discord, after the gateway picks up the plugin:
/olp status
/olp usage
/olp models
/olp help
Output is wrapped in a monospace code block. Long responses are truncated
to fit Telegram's ~4096-char per-message limit; a ... [truncated, use SSH for full] suffix marks where the cut happened.
Authorization model
The plugin sends Authorization: Bearer <apiKey> on every request. OLP's
server enforces:
- public-tier endpoints (
/health,/v1/models) accept any non-revoked key (or no key at all ifauth.allow_anonymous: true). - owner-tier endpoints (
/v0/management/*,/cache/stats) reject any non-owner key with 403.
If you see 401 unauthorized or 403 forbidden in chat:
- Verify the configured
apiKeyis a non-revoked owner-tier key. - Verify the key was created on the same host running the OLP server (keys
are stored under
~/.olp/keys/and validated by hash on the server side). - Check the OLP server's
/healthdirectly withcurlto confirm reachability.
Port resolution priority
OLP_PROXY_URLenv (full URL) — useful when the gateway runs on a different host than OLP and you proxy in via Tailscale.OLP_PORTenv (port only; localhost assumed).- Plugin config
proxyUrl. - Fallback
http://127.0.0.1:4567.
Why no Telegram/Discord SDK dependency
OpenClaw provides the transport (Telegram bot + Discord bot are gateway features). This plugin only registers a slash command — it does not open its own websocket / long-poll connection. That means:
- No new npm dependency.
- No bot tokens stored in plugin config.
- Plugin works for any OpenClaw-supported chat surface (currently Telegram + Discord; future surfaces inherit automatically).
Cross-references
- Local
olpCLI — the full mutating-capable surface. - OCP
/ocpplugin — the OCP predecessor this is ported from. - ADR 0010 — Phase 4 charter.
- ADR 0007 — multi-key auth model that gates owner-tier subcommands.