Type: RFC Status: Accepted Systems: Plugins, Gateway, Config, Sources Author: Phil / Claude Date: 2026-07-31 Related: LLP 0109, LLP 0143, LLP 0144, LLP 0145, LLP 0146, LLP 0148, LLP 0149, LLP 0152, LLP 0157, LLP 0161, LLP 0162; issues #539, #543, #544 Spawned: LLP 0168 (override replaces steering), LLP 0169 (attach surface returns), LLP 0170 (scheduled sweep), LLP 0171 (requirements spec), accepted 2026-07-31
Proposal: replace the OpenClaw steering plugin with two lanes that need nothing installed on the OpenClaw side. Lane A (live wire):
hyp attach --client openclawwrites abaseUrloverride for theanthropicandopenaiproviders in~/.openclaw/openclaw.json, pointing them at the local AI gateway and carrying thex-hypaware-upstreamheader as a static config value. Lane B (transcript sweep): the existing session-file backfill runs on a 5-minute schedule, so every OpenClaw turn on every provider is captured at transcript fidelity, andpart_iddedupe nets the overlap with lane A to zero. The steering plugin package and everything that exists only to serve it (credential borrowing, the wire-parity mirror, the live warning ledger) are deleted, roughly 2,100 lines. The gateway, projector, settlement, and backfill projection are unchanged.
OpenClaw supports a per-provider config override: an explicit
models.providers.<id> entry merges over the built-in catalog entry,
including baseUrl and headers
(openclaw repo, src/agents/models-config.plan.ts
resolveProvidersFromConfig / mergeProviders, explicit wins;
src/config/types.models.ts ModelProviderConfig carries
baseUrl and headers). Overriding anthropic and openai steers every
model ref on those providers, primary, fallbacks, per-agent overrides, the
auxiliary model slots, and runtime /model switches within the provider,
because they all resolve through the same provider entry.
That is the same two-provider coverage the steering plugin delivers (LLP 0144 restricts steering to the canonical vendors anyway), obtained through a documented config seam instead of an OpenClaw-side npm package that is opt-in and not yet published.
- Distribution. The steering plugin only works where someone ran
openclaw plugins install. It is not on npm. Most installs will never have it, so the live lane of LLP 0157's coverage statement is aspirational on a typical machine. A config write ships withhyp attachand needs nothing from the user's side. - This option was never considered. LLP 0152 rejected "widen the
settings-file edit", meaning rewriting every
provider/modelref, which is unbounded and goes stale per ref. A per-providerbaseUrloverride was not among its options. It does not share the fatal flaw: it steers at the provider level, so refs never need rewriting and new refs on an overridden provider are covered from birth. - Two whole decision surfaces become unnecessary. The shadow providers
had no credentials and no vendor-specific header shaping, so LLP 0145
(credential borrowing) and LLP 0148 (the
wrapStreamFnwire-parity mirror) exist to reconstruct what the real provider already does. With the real provider overridden, its own auth (API key or pasted OAuth setup-token) and its own header shaping apply natively. The mirror was the most drift-prone code in the plugin; it is deleted, not ported. - Attach state becomes observable again. A config entry is a probeable
artifact, so
hyp statuscan report OpenClaw attach state truthfully, resolving the misleading permanentattach openclaw [pending](#544).
Attach writes (or merges into) two entries in ~/.openclaw/openclaw.json:
{
"models": {
"providers": {
"anthropic": {
"baseUrl": "http://127.0.0.1:18521",
"headers": { "x-hypaware-upstream": "anthropic" },
"models": []
},
"openai": {
"baseUrl": "http://127.0.0.1:18521/v1",
"headers": { "x-hypaware-upstream": "openai" },
"models": []
}
}
}
}(Shape verified live; see the results under {#verify}. The gateway port
is the fixed default of LLP 0114. models: [] is mandatory: OpenClaw's
config schema types models as a required array and hard-refuses CLI
commands on a schema-invalid config; an empty array passes validation
and does not empty the built-in catalog. The /v1 on the openai
entry is also mandatory: the OpenAI client appends /responses or
/chat/completions to baseUrl, so a bare origin would produce paths
outside the gateway's /v1 prefix, while the Anthropic client appends
/v1/messages itself and wants the bare origin.)
The static x-hypaware-upstream header is the load-bearing trick: it is
the same header the steering plugin attached per request, so the gateway's
upstream precedence rung and the projector's shape/attribution gate
(projector.js, LLP 0161#upstream-presets and #projector-shape) keep
working byte-for-byte. No gateway or projector change is part of this
proposal.
-
Attach refuses over a user's entry, and rewrites its own. If the user already declares
models.providers.anthropicor.openai, attach refuses with an explanation: those keys are user-authored (verified: no OpenClaw code writes them; a default install has none), so their presence means the user deliberately routed that provider somewhere, and silently rerouting a deliberate override is the surprise this design family refuses to allow. Otherwise attach writes the two entries whole.The one entry that is not a user's is the one attach itself wrote, and it must be overwritable, because attach re-runs.
isCurrent()makes adoneattach stale whenever the gateway rebound to a new ephemeral port (LLP 0086) or the contributed asset set changed (LLP 0107), and the reconciler then re-performs; a presence-only refusal would fail every one of those passes forever, leavingopenclaw.jsonpinned to a dead port while the marker-header probe still reportedattached. So the refusal is ownership-aware, on the same self-identifying triple detach already tests before deleting (baseUrl,headers['x-hypaware-upstream']naming the key, emptymodels), out of one shared predicate (src/core/config/provider_entry_ownership.js) so the two halves cannot drift. Attach's call passes no expected base URL, deliberately: on a drift re-attach its own entry carries the old origin.Detach deletes an entry only when its
baseUrlis the gateway's; a present-but-not-ours or mangled entry is backed up, never discarded (LLP 0163 precedent). Nothing a user authored is ever displaced, so no prior state is stored and no undo record exists anywhere: deletion is the whole undo. -
The marker is the entry itself. The
x-hypaware-upstreamheader inside the created entry is the probeable marker. The manifest regainscontributes.client.attach_probein thejson_pathformat, and core restores thejson_pathbranches #510 deleted (src/core/config/client_detach_disk.js,src/core/daemon/status.js), reversing that half of LLP 0143. A plain top-leveljsonmarker key is NOT usable: OpenClaw's config is strictly validated and an unknown top-level key stops its gateway from starting, the verified LLP 0109 finding that motivatedjson_pathin the first place. -
Attach-on-join returns, full symmetry (settled at grill, 2026-07-31): the plugin registers the runtime clients adapter so the LLP 0044 loop covers OpenClaw exactly as it covers Claude/Codex, governed by the already-declared
attach.on_joinpolicy. A refuse-on-existing hit during join surfaces as a warning and never fails the join. -
Detach rewrites the per-agent caches; they do not self-heal (verified live, {#verify} item 3). Once a provider entry has been written into
~/.openclaw/agents/<id>/agent/models.json, removing the config entry does not remove it: the models.json plan runs inmergemode and carries every existing cache provider forward wholesale (mergeWithExistingProviderSecretsinsrc/agents/models-config.merge.ts), so the gateway URL survives regeneration indefinitely and remains live for routing (a stale entry kept steering turns in the live test). Detach must delete the entries it wrote from every agent'smodels.json, not just the default agent's; deleting the provider key from the cache file is sufficient and verified not to resurrect on the next regeneration. -
Attach and detach print a restart instruction. On the installed OpenClaw (2026.3.13), a running gateway does not apply
models.providerschanges to subsequent turns; turns kept using the oldbaseUrluntil restart ({#verify} item 4). Attach and detach must end with "restart the OpenClaw gateway (openclaw gateway restart) to apply".
Settled at grill (2026-07-31): the session-file backfill provider runs
on a daemon schedule, every 5 minutes by default, tunable in the
plugin's existing backfill config section. The daemon already runs
cron-matched periodic work (the sink driver,
src/core/sinks/driver.js), so this is scheduling an existing job, not
a new primitive. Re-runs are cheap no-ops through the existing part_id
dedupe.
The sweep is what makes "lane A is not recording" a self-healing state rather than a detectable one. It silently covers, with no mode switch and no detection logic:
- machines never attached (or where refuse-on-existing fired);
- the restart-pending gap ({#verify-results} item 4): override written,
openclaw gateway restartnot yet run, turns flowing direct; - every provider outside the big two: the LLP 0146 families (Bedrock, Vertex, Google, per-account-URL gateways), ollama, and any future provider all land in the session file identically, so they are captured at transcript fidelity. The LLP 0146 deferral list stops describing a coverage hole and starts describing only which lane serves a provider;
- pre-attach history and any missed window (backfill's original job).
When both lanes capture a turn, flush-time settlement upgrades the live
row to the file's native message id, the sweep's twin lands on the same
part_id, and dedupe resolves it to zero writes: the wire-fidelity row
survives. This is why the settlement/match-key machinery is kept,
not deleted; it is the hinge the hybrid turns on.
Settlement race and quiesce window. A sweep that imports a turn before the live row's settlement has flushed would land the native id first and collide instead of deduping. The sweep therefore skips session files whose mtime is within a quiesce window (default: the settlement flush interval plus margin). The cost is nothing: a recently-active session is exactly the one lane A is capturing in real time, and on unattached machines there is no live twin to race.
The live pass-through warnings (LLP 0149) die with the plugin, and no
replacement measurement is needed (settled at grill, 2026-07-31): with
the sweep, essentially nothing escapes, and what each lane covers is
clear from config inspection alone. The backfill's excluded_backend
events remain, but as the LLP 0147 sibling-territory boundary marker
(claude-cli / codex turns belong to the transcript adapters), not as a
coverage ledger.
The coverage statement becomes: every OpenClaw turn is captured at
least at transcript fidelity within the sweep interval; anthropic/*
and openai/* turns are additionally captured live at wire fidelity
when attached.
The cleanup is part of the change, not a follow-up. In the same change set:
- Delete the
openclaw-steering-plugin/package entirely:src/(steering.js,runtime_auth.js,wire_parity.js,warning_ledger.js,gateway_endpoint.js,index.js, the.d.tsfiles),test/,openclaw.plugin.json,package.json. 2,080 lines. - Delete
test/plugins/openclaw-steering-plugin.test.js(the suite wrapper, 24 lines). - Rewrite
docs/ACCEPTANCE.mdopenclaw_capture: theopenclaw plugins install --link/plugins enablesteps becomehyp attach --client openclawplus a config-content check; the "shadow providers registered and steering" assertion becomes "override entries present and turns route through the gateway". A sweep step is added: a turn on a non-overridden provider must land via the scheduled sweep within the interval, with zero duplicate rows for a turn both lanes captured. - Rewrite the picker and manifest copy in
hypaware-core/plugins-workspace/openclaw/hypaware.plugin.json: no more "install the @hypaware/openclaw-steering-plugin package"; see {#onboarding}. - Corpus updates (spawn shape settled at grill, 2026-07-31;
spawned on acceptance, same day): LLP 0168 (config override
replaces plugin steering; supersedes LLP 0152, moots LLP 0145 and
LLP 0148), LLP 0169 (the attach surface returns; supersedes
LLP 0143:
json_pathrevival, refuse + create-only, cache rewrite on detach, attach-on-join symmetry), LLP 0170 (the scheduled transcript sweep; the 5-minute lane, the quiesce window, retires LLP 0149's ledger), and LLP 0171 (the R-numbered requirements spec replacing 0157's dead half, the role 0157 played for #510). Each superseded doc carries its forward ref; LLP 0157/0161/0162 carryExtended-bynotes. LLP 0144's shape/canonical-vendor analysis and LLP 0146's deferral list carry over unchanged as live-lane rationale. - Spec 0157 requirement disposition, so the amendment is
requirement-precise rather than section-vague: R1-R6 die with the
steering plugin (they specify its providers, steering, auth, mirror,
pass-through, and header plumbing; the header now arrives from config).
R7 is reversed outright (an
attach_probereturns and core regainsjson_path). R8, R9, R11, R14 survive untouched (projector shapes, shared reader, route-identity dedupe, settlement policy drop). R10 survives untouched (backfill policy gate and CLI-backend exclusion). R12 survives with the acceptance procedure rewritten per this RFC. R13 is retired with the ledger: the sweep makes the coverage statement of {#sweep-lane} and no escaped-traffic measurement is maintained. - Keep, unchanged: everything under
hypaware-core/plugins-workspace/openclaw/src/(projector including the header gate, settlement, match key, session-file reader, backfill, config), and the gateway presets with theirx-hypaware-upstreamrungs. Issue #543's reader fix proceeds independently of this RFC.
Net effect: roughly 2,100 lines deleted. The additions are one create-only settings-write module (no undo record; deletion is the undo, {#attach-detach}) plus the cache rewrite on detach, and the 5-minute schedule with its quiesce window around the existing backfill provider ({#sweep-lane}); no new capture lane is implemented.
Two picker line items change:
- Claude: state plainly that it captures anything that runs the
Claude Code CLI / Claude Agent SDK, and name the OpenClaw case
explicitly: an OpenClaw configured with a
claude-cli/<model>backend (the default on a subscription machine) is captured here, asclaude, with no OpenClaw-side setup. - OpenClaw: enumerates what the adapter collects, and states it in
two clear tiers: ALL OpenClaw conversations are captured from the
local session transcripts (every provider, within the sweep
interval), and
anthropic/<model>andopenai/<model>turns are additionally captured live at wire fidelity through the gateway via the config override that attach writes.
- Overriding any provider beyond
anthropicandopenai. When HypAware adds gateway support for another provider family, or a user installs an OpenClaw provider plugin, the follow-up is a prompt to add that provider's override to the config. This is now purely a fidelity upgrade (live wire capture instead of the sweep's transcript capture), never a coverage gap: the sweep already records those providers' turns. The LLP 0146 deferrals stand unchanged as live-lane deferrals. - An
fs.watch-based live tail of the session files, as a latency upgrade behind the same LLP 0158 reader, if the 5-minute sweep interval ever matters. Out of scope now; the repo has no watcher primitive and the sweep needs none. - The
*.trajectory.jsonlfiles and the probe-session question, tracked in #543's adjacent-decisions list. - A refuse-instead-of-capture-nothing mode (unchanged non-goal from LLP 0149/0157).
- Correlating an OpenClaw session with the child CLI session it spawned (unchanged non-goal, LLP 0147).
Load-bearing facts to confirm against a live OpenClaw before implementation; each maps to an acceptance step:
- Merge keeps the catalog's models and auth. An explicit
models.providers.anthropicentry that sets onlybaseUrlandheadersmust inherit the built-in model list and auth modes rather than clobber them. (ModelProviderConfig.modelsis typed required; confirm a partial entry merges rather than replaces, and if a minimalmodels: []is needed, confirm that does not empty the catalog.) - URL shape per provider. What path the anthropic and openai clients
append to
baseUrl(/v1/messages,/chat/completions, with or without a/v1base segment), so the override URL lands on the gateway's path prefixes. - Cache self-heal on detach. Remove the override, regenerate, and
confirm no agent
models.jsonretains the gateway URL. - Config reload semantics. Whether a running OpenClaw picks up the override without a restart, and what attach should print if not (the acceptance procedure currently restarts the OpenClaw gateway after plugin changes).
- OAuth setup-token profiles ride through. A pasted subscription
token on the
anthropicprovider authenticates through the gateway unchanged (native shaping, no mirror). - Interaction with a configured
openaigateway upstream (#539). With the header now arriving from config, confirm routing behavior when an operator declares their ownopenaiupstream, and whether #539 is mooted, narrowed, or unchanged for OpenClaw traffic.
All six items were executed against the real install on this machine
(binary 2026.3.13, source checkout 2026.4.2 for code reading; note
docs/ACCEPTANCE.md floors openclaw_capture at 2026.4.24+, so the
acceptance run must re-confirm items 1, 3, and 4 on a current binary).
Method: config edits on the live ~/.openclaw/openclaw.json (backed up
and restored), one-turn probes against a local logging listener, an
isolated --profile install for gateway tests, and a hermetic
workspace-build gateway (temp HYP_HOME, no daemon install) for
item 6.
- Merges, but only with
models: []- CONFIRMED with a caveat. A partial entry with onlybaseUrl+headersis rejected by schema validation (models.providers.anthropic.models: Invalid input: expected array, received undefined) and a schema-invalid config makes the CLI hard-refuse (Config invalid ... Run: openclaw doctor --fix). Withmodels: []the entry validates, the full built-in anthropic catalog survives inmodels list --all, and auth still resolves. Attach MUST writemodels: []. Cosmetic side effect: a loopbackbaseUrlflips the models'Localcolumn toyesinopenclaw models list. - URL shape is asymmetric - CONFIRMED live. The anthropic client
(official Anthropic SDK via pi-ai) appends
/v1/messagesto a bare-originbaseUrl; the openai client appends/responses(or/chat/completions) to abaseUrlthat must already carry/v1. So: anthropic override =http://127.0.0.1:18521, openai override =http://127.0.0.1:18521/v1. Both observed at a local listener; the config-sourcedx-hypaware-upstreamheader rode on every request. Failure-mode note: against an erroring endpoint the embedded runtime retried the turn about 12 times before surfacing the error. - No self-heal - FALSIFIED, design amended. See the amended
{#attach-detach}: the cache carries stale entries forward
indefinitely and they stay live for routing. Detach rewrites every
agent's
models.json. - No pickup without restart - CONFIRMED (restart required). With
a running gateway,
models.providersbaseUrl changes were not applied to subsequent turns (tested with both file-replace and in-place writes; no reload event logged). The source checkout (2026.4.2) has a chokidar config reloader whose plan marksmodelsas hot-reloadable, so newer binaries may pick it up, but attach must not rely on it: it prints the restart instruction ({#attach-detach}). - Setup-token rides through - CONFIRMED. The machine's
anthropic:defaultauth profile (typetoken, a pasted subscription setup token) authenticated a turn against the overriddenbaseUrlnatively: the request carriedauthorizationplusanthropic-betaheaders (names verified at the listener, values never logged) and the run log shows the profile applied. Native shaping, no mirror, exactly as {#motivation} argues. - #539 is mooted for OpenClaw traffic - CONFIRMED hermetically.
Workspace-build gateway with an operator-configured
openaiupstream (nomatch(), so the steering rung is gone, the #539 seam): an RFC-shaped turn (/v1/responses,/v1/chat/completions) still routes to the configured upstream by path prefix and the turn succeeds; the old steering-plugin shape (/chat/completions, bare origin) 404s withno upstream matches path, reproducing #539's failure mode. The header is stripped before the request leaves the gateway on the config-upstream route too, so nothing leaks to the operator's endpoint, and capture attribution is unaffected because the projector gate reads the header from the recorded inbound request. #539 remains open only as a steering-plugin-era concern; this design does not hit it, provided the/v1shape from {#override-entries}.
Incidental finding, evidence for the {#open-questions} stranded-attach
residual: this machine still carried a dead shadow-provider attach from
2026-07-24 (a models.providers.hypaware entry pointing at the
uninstalled gateway, plus the rewritten primary model ref). Because it
used string entries in models, it also made the whole config
schema-invalid, which hard-blocked openclaw models list wholesale.
The RFC's entries validate cleanly (models: []), so a stranded RFC
attach degrades to connection-refused turns only, not a bricked CLI
surface. The stale attach was undone during verification per its own
managed marker.
Refuse or merge-with-undo on a pre-existing provider entry?Settled (grill, 2026-07-31): refuse + create-only; see {#attach-detach}.DoesSettled (grill, 2026-07-31): no new machinery.hyp leave/ uninstall need to force-detach OpenClaw?hyp leaveand manual detach both route through the one probe-driven core undo (src/core/commands/clients.js, LLP 0138#marker-undo), so a probed OpenClaw is covered by the shared path automatically. The residual, a user removing HypAware without ever detaching, strands a dead gateway URL and turns fail loudly at connection time; that exposure is identical to Claude's stranded attach block today and is accepted as the same class, not new hazard.
- LLP 0152 (the mechanism this replaces), LLP 0143 (the no-attach posture this reverses), LLP 0145 / 0148 / 0149 (machinery mooted or re-laned)
- LLP 0144 (shape and canonical-vendor analysis, carried over), LLP 0146 (deferrals, carried over), LLP 0157 / 0161 / 0162 (the shipped change set this amends)
openclawrepo:src/agents/models-config.plan.ts,src/agents/models-config.merge.ts,src/config/types.models.ts- Issues #539, #543, #544