Skip to content

llp: OpenClaw full capture — plugin-steered shadow providers replace the settings edit (LLP 0142–0149) - #475

Merged
philcunliffe merged 1 commit into
masterfrom
llp/openclaw-full-capture
Jul 30, 2026
Merged

llp: OpenClaw full capture — plugin-steered shadow providers replace the settings edit (LLP 0142–0149)#475
philcunliffe merged 1 commit into
masterfrom
llp/openclaw-full-capture

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

What

Eight Draft decisions revising the @hypaware/openclaw adapter (LLP 0109) so capture covers all bearer-token providers — OpenRouter, OpenAI, Groq, and the rest — instead of one repointed Anthropic primary. Docs only; no code changes in this PR.

Why

LLP 0109's mechanism (edit openclaw.json, repoint model.primary) captures a silent subset: fallbacks, per-agent models, image/PDF slots, and runtime /model overrides all escape uncaptured, with nothing marking the gap. Its ${ANTHROPIC_API_KEY} auth also breaks for subscription users — OpenClaw's most common Anthropic auth. Both problems are structural to the settings-edit approach.

The decisions

LLPDecision
0142Steering moves to an OpenClaw-side plugin: catalog-contributed shadow providers + before_model_resolve. The AI gateway proxy stays the capture path — fidelity is unchanged.
0143No attach_probe; detach is an honest no-op (Desktop precedent, #445); the json_path core format retires — OpenClaw was its last consumer. Breaking change accepted; no installed base.
0144One shadow provider per API shape (anthropic-messages / openai-completions), not per vendor. Session identity keeps the prompt-head hash for v1.
0145Shadow providers borrow the shadowed provider's credential via the public plugin-sdk resolveApiKeyForProvider — fixes subscription-auth users.
0146Host-signed providers (Bedrock, Vertex) and per-account proxy vendors (Cloudflare/Vercel AI gateways) are deferred, not solved.
0147CLI-backend turns (claude-cli/*, codex) are permanently transcript-captured by the sibling adapters, never proxied.
0148Wire parity (anthropic-beta set, service_tier) is mirrored in the plugin's own wrapStreamFn. The existing hypaware.ai-gateway ^2.0.0 capability is reused unchanged — header-driven routing already exists in matchUpstream.
0149Uncapturable turns pass through and warn (no_credential / no_preset / deferred) — one rule for every cause; the warnings are the coverage ledger.

Verified against OpenClaw source (not guessed)

  • before_model_resolve re-fires per fallback candidate (model-fallback.ts loop → runAgentAttemptresolveHookModelSelection) and covers subagent spawns (callGateway({method:"agent"}) re-enters the same pipeline)
  • resolveApiKeyForProvider's OAuth branch returns live tokens, refreshing under a lock when expired (auth-profiles/oauth.ts)
  • Profile eligibility hard-fails on provider mismatch — the credential borrow must happen in prepareRuntimeAuth, not via profile ordering
  • The models.json baseUrl-preservation hazard is neutralized by the fixed gateway port (LLP 0114) plus per-request baseUrl from prepareRuntimeAuth (GitHub Copilot precedent)
  • Known regression vs 0109, named in 0142: OpenClaw's PDF tool does a raw fetch gated on provider === "anthropic" and bypasses steering — a low-volume side channel, documented rather than silent

Notes for review

  • All eight are Draft; 0142 supersedes 0109's mechanism but 0109's status is left to its author
  • ref-check: the new docs contribute zero errors (the corpus's 116 pre-existing errors are untouched)
  • One [inferred] remains, in 0147's open question (session-correlation follow-up) — legal in a Draft
  • Remaining parked questions: hyp status attach signal for probe-less clients (0143), vendor-ToS on proxied credentials (0145), whether pi-ai adds default betas itself (0148, implementation-time check)

🤖 Generated with Claude Code

…the settings edit (LLP 0142-0149)
Eight Draft decisions revising the @hypaware/openclaw adapter (LLP 0109)
so capture covers all bearer-token providers instead of one repointed
Anthropic primary:
- 0142 steering moves from mutating openclaw.json to an OpenClaw-side
plugin: catalog-contributed shadow providers + before_model_resolve
(verified to re-fire per fallback candidate and cover subagents)
- 0143 no attach_probe; detach is an honest no-op; json_path retires
(OpenClaw was its last consumer; breaking change accepted, no
installed base)
- 0144 one shadow provider per API shape (anthropic-messages /
openai-completions), not per vendor; session identity stays the
prompt-head hash for v1
- 0145 shadow providers borrow the shadowed provider's credential via
the public plugin-sdk resolveApiKeyForProvider (OAuth branch verified
to return live, refreshed tokens) through prepareRuntimeAuth
- 0146 host-signed providers (Bedrock, Vertex) and per-account proxy
vendors (Cloudflare/Vercel AI gateways) deferred, not solved
- 0147 CLI-backend turns (claude-cli/codex) are permanently
transcript-captured by the sibling adapters, never proxied
- 0148 wire parity (anthropic-beta set, service_tier) is mirrored in
the plugin's own wrapStreamFn; the existing ai-gateway capability is
reused unchanged
- 0149 uncapturable turns pass through and warn (no_credential /
no_preset / deferred), one rule for every cause
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@philcunliffe
philcunliffe merged commit 627e6d6 into masterJul 30, 2026
8 checks passed
@philcunliffe
philcunliffe deleted the llp/openclaw-full-capture branch July 30, 2026 03:15
philcunliffe pushed a commit that referenced this pull request Jul 30, 2026
Two things, both forced by what landed on master while this was in review.
## The conflict: #458 rewrote the file this PR collapses
#458 ("hyp session ignore names the session container Codex drops on") and this
PR both touch `readRolloutMeta` in `ai-gateway/src/session_command.js`, from
opposite directions. #458 grew it: a `type === 'session_meta'` envelope guard, a
third field (`payload.session_id`), a blank-after-trim test on that field, and a
new resolution path (`resolveFromStatedThread`) built on it. This PR deletes it:
issue #465's whole value is that no second copy of the `session_meta` rules
survives, so the function becomes a delegation to
`src/core/codex/rollout_session_meta.js`.
Resolved by keeping both. Every behaviour #458 added is intact - the container
is still the answer, `CODEX_THREAD_ID` is still a selector rather than an answer,
a legacy or blank `session_id` still refuses rather than falling back to the
thread id - and none of the predicates behind it live here any more. The three
rules (raw line, envelope type, blank-is-absent) are stated once, in the core
reader, which is the point of #465.
## Third-copy check
#458's own resolution logic does NOT read `session_meta` fields directly.
`resolveFromStatedThread` matches on `meta.threadId` and reads `meta.sessionId`,
both from `readRolloutMeta`, so it is a second *resolution path*, not a second
*reader*, and routing it through the shared reader needed no change to it. What
had become a full second copy is `readRolloutMeta` itself: #458 gave it its own
envelope guard and its own blank test on the new field, which is precisely the
duplicate this PR removes. `statedEnv` is a blankness test on an environment
variable, not on the header, and stays.
The only other site that reads `session_meta` fields itself is
`codex/src/backfill.js`, which walks whole rollout files (folding `turn_context`)
and so cannot call a first-line reader. It shares the one `cwd` predicate
(`sessionMetaCwd`) and deliberately does not share rule 3's id refusal: a
backfilled row must land in some partition, where the CLI can refuse. Now stated
in the LLP rather than only in the code comment.
## One behaviour change the merge required
`readRolloutMeta` no longer discards a rollout whose `cwd` is unusable; it passes
`cwd: undefined` through. The shared reader refuses a blank or relative `cwd`
(LLP 0150 #usable-cwd), and on the cwd-matching path that is what we want. On
#458's stated-thread path `cwd` is never consulted, so requiring it would have
turned a field-level predicate into a file-level one and refused a session whose
container is plainly on disk - a regression of #458 introduced by tightening a
field it does not use. Pinned by a new test, mutation-checked: restoring the cwd
requirement reddens it.
## Reconciled beyond the conflict markers
Four claims that auto-merged cleanly but stopped being true once #458 landed:
- LLP 0150 said `sessionId` "has no consumer yet on purpose" and that moving the
verb onto the container "is #453's job". #453 is closed and the verb is moved;
the section now records that, and that #458 added a resolution path but no
second reader.
- LLP 0150 said the `hyp session` caller "compares `meta.cwd` against an absolute
invocation cwd, so a relative value never matched". True of one of its two
paths now. Rewritten to say why the other path must not refuse on `cwd` at all.
- LLP 0150's rule 3 read as a blanket "callers refuse", which the backfill does
not. Scoped to the reader, with the backfill's different answer explained.
- The `readRolloutMeta` doc comment said `meta.sessionId` "is deliberately not
consulted here". It is consulted now.
Also: LLP 0150's Context bullet no longer implies `CODEX_THREAD_ID` makes the
rollout unnecessary, and `resolveSessionIdForCli`'s legacy-rollout note points at
`legacyRolloutError`, which is where that refusal now lives.
## The renumber: collision avoidance, NOT a ruling on #469#475 landed `llp/0143-openclaw-registers-no-attach-probe.decision.md` while this
PR held `llp/0143-one-reader-for-codex-session-meta.decision.md`. Different
filenames, so git flags no conflict, but merging as-is would put two documents at
0143 on master, a fifth duplicate after 0098, 0099, 0111 and 0142. This document
moves to 0150 (0149 is the highest on master) and all 12 references follow:
`@ref` annotations in `session_command.js`, `backfill.js`, `rollout-cwd.js`,
`rollout_session_meta.js`, `types.d.ts` and four test files, plus the heading and
one self-reference in the document.
**This is mechanical collision avoidance and sets no precedent.** Issue #469
asks whether the later claimant renumbers or whether citations become
filename-qualified, and that question is still open and unowned. Renumbering here
is only what avoids adding a sixth duplicate today; whichever way #469 is
decided, nothing about this commit should be read as having decided it. The
human's 0143 is untouched, as are LLP 0142's two references to it.
Verified: `npm test` 2915 tests, 2905 pass, 8 fail (the pre-existing
`test/core/leave-command.test.js` set, identical on a pristine `origin/master`
worktree), +12 tests and no new failures. `npm run typecheck` clean. Smokes
`gateway_codex_capture`, `session_optout_capture_drop`, `backfill_codex_fixture`
all ok. All `@ref LLP 0150` targets and anchors resolve.
Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe pushed a commit that referenced this pull request Jul 30, 2026
PR #475 merged llp/0144-shadow-provider-per-api-shape.decision.md to master as
part of the OpenClaw 0142-0149 block, so this branch's
llp/0144-codex-lineage-from-body-client-metadata.decision.md would have put two
documents at 0144. Because the filenames differ, git reported no conflict and CI
stayed green, so nothing on the rung ladder would have caught it before merge.
0151 verified free across master and every remote branch. 0150 is held by
fix/issue-465, renumbered there from 0143 for the same reason.
Purely a renumber: the document's content is unchanged, and the rename carries
every reference with it (12 @ref annotations in exchange-projector.js, 1 in the
gateway_codex_capture smoke, 14 in codex-exchange-projector.test.js, 1 in
codex-rollout-cwd.test.js, plus the cross-links and Related entries in LLP 0083
and LLP 0141, and the heading). No residual 0144 reference remains on this
branch and all six referenced anchors resolve in the renamed document.
The human directed this renumber explicitly, accepting that moving the head
strips neutral:approved and re-opens the review ladder. It resolves this one
collision and sets no precedent for issue #469, where the general
renumber-versus-qualified-citation convention is still open.
Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe added a commit that referenced this pull request Jul 30, 2026
…s Codex never sends (#464) (#467)
* Codex lineage reads the durable body client_metadata (#464)
The Codex live projector derived a turn's thread, session and parent thread
from request headers, three of whose names Codex has never emitted, while the
authoritative ids sat unread in the request body.
Read against Codex's source, Codex projects one `CodexResponsesMetadata`
snapshot onto three surfaces per HTTP request. The flat body `client_metadata`
map (a top-level field of `ResponsesApiRequest`) is built unconditionally and
always carries `session_id` and `thread_id`; the `x-codex-turn-metadata` blob
only rides along for request kinds that carry turn metadata; and
`compatibility_headers` emits exactly four names. `thread-id`, `session-id`
and `parent-thread-id`, the three bare names the projector read, are not among
them, so they could never supply a right value and could supply a wrong one:
any hop setting `thread-id` dictated `conversation_id`, the scope of the row's
fallback `message_id`.
- Lineage now resolves body-map-first, turn-metadata-blob-second, with the real
`x-codex-parent-thread-id` header (previously misspelled) last.
- A Codex-owned `client_metadata` map is itself a sufficient signal that an
exchange is Codex, so the API-key route's generic `/v1/responses` no longer
needs a Codex header to be recognized.
- The three fictional header names are gone; the real ones are named constants.
- `attributes.codex.lineage_source` records which surface stated the identity,
so a future Codex version dropping one is queryable rather than a silent
`conversation_id` drift.
Nothing re-keys: the blob's `thread_id` and the body map's `thread_id` are the
same field of the same snapshot, and the removed header names never matched
real traffic, so `conversation_id` (and the `message_id` / `part_id` scoped on
it, LLP 0030) is unchanged for every shape already recorded. Already-recorded
rows are left alone; LLP 0143 states why no backfill.
LLP 0143 is the decision doc. LLP 0083 and LLP 0141 carried the disproved
premise that `x-codex-turn-metadata` is Codex Desktop behavior and that the
subscription route states its session in a `session-id` header; both are
corrected, and the two `codex-rollout-cwd` fixtures that rested on that header
now use the shape Codex really sends.
Co-Authored-By: Claude <noreply@anthropic.com>
* Renumber LLP 0143 to 0144 to clear a duplicate number with PR #466
PR #466 (fix/issue-465) independently minted llp/0143-one-reader-for-codex-session-meta
on its own branch at the same time this branch minted
llp/0143-codex-lineage-from-body-client-metadata. Two documents cannot share a
number, and duplicate LLP numbers are exactly the corpus defect issue #463
tracks, so this branch takes 0144 (deterministic tie-break: the lower PR number
keeps the original).
Purely a renumber: every @ref anchor, cross-link, and Related entry follows the
move, and the document's own content is unchanged. Verified no residual "0143"
reference remains on this branch and all six referenced anchors still resolve in
the renamed document.
Co-Authored-By: Claude <noreply@anthropic.com>
* Codex lineage: record a body/blob disagreement instead of silently preferring one
The body-map-first precedence rests on Codex projecting one metadata snapshot
onto both the flat `client_metadata` map and the `x-codex-turn-metadata` blob,
so that the two are equal whenever both are present. That is a claim about
another program's internals which HypAware cannot verify, and the body-wins
tie-break discarded the counter-evidence without trace: a row whose surfaces
disagreed was indistinguishable from a row whose surfaces agreed.
- `attributes.codex.lineage_conflict` now names the lineage fields the two
surfaces state differently (`thread_id`, `session_id`, `turn_id`,
`parent_thread_id`), absent when they agree or only one spoke. The row still
keys on the body, so this adds a signal and moves no identity.
- `lineage_source` now resolves in the same order as the values it describes
(`thread_id` before `session_id`, body before blob). It previously answered
"did the body state anything at all", which mislabelled a turn whose
`thread_id` came from the blob while only its `session_id` came from the body
as `body_client_metadata`, though `conversation_id` keys on `thread_id`.
- LLP 0144 gains `#lineage-conflict` and states why the assumption gets a
continuously checked signal rather than a one-time assertion.
Co-Authored-By: Claude <noreply@anthropic.com>
* Codex lineage: an ambiguous flat client_metadata pair is not evidence of Codex
`readCodexClientMetadata` accepted a body `client_metadata` map on either of two
signals: an `x-codex-*` prefixed key, or the flat `session_id` + `thread_id`
pair. Only the first is Codex-exclusive. The second is a shape any agent
framework may send, and the projector's matched path set includes the fully
generic `/v1/responses` and `/v1/chat/completions`, so an unrelated client that
posted that pair was stamped `client_name: 'codex'` and dictated the row's
`conversation_id` and `session_id` (the partition key, LLP 0030). That is the
same defect class as the fictional `thread-id` header this branch removed,
reached through the body instead of a header, and in a capture product a misfiled
client is a privacy question.
The flat pair is now honoured only when the transport already identified the
exchange as Codex independently of the body (`hasCodexTransportSignal`: the
`chatgpt` upstream, the `/backend-api/codex/` namespace, an `x-codex-*`
compatibility header, or a `codex`-prefixed user-agent product). Real Codex loses
nothing: `client_metadata` carries `x-codex-installation-id` and
`x-codex-window-id` on every request, so the strict branch alone covers all known
Codex traffic, and the corroborated pair still covers a build that stopped
writing them. A non-Codex client's row now comes out byte-identical to the same
request with no `client_metadata` at all.
`isCodexExchange` is replaced by `hasCodexTransportSignal` plus the body check at
the single decision point in `resolveCodexContext`, so the corroboration flag
cannot drift between the two callers.
Also pins the assumption that keeps the outer `match` gate (path and
turn-metadata header only) consistent with the body being a Codex signal: a test
asserts every route Codex posts to passes the gate, so a body-only Codex request
is never dropped before the body is read.
LLP 0144#body-is-a-codex-signal and #body-is-authority are amended in the same
commit.
Co-Authored-By: Claude <noreply@anthropic.com>
* Renumber LLP 0144 to 0151 to clear the collision with the OpenClaw block
PR #475 merged llp/0144-shadow-provider-per-api-shape.decision.md to master as
part of the OpenClaw 0142-0149 block, so this branch's
llp/0144-codex-lineage-from-body-client-metadata.decision.md would have put two
documents at 0144. Because the filenames differ, git reported no conflict and CI
stayed green, so nothing on the rung ladder would have caught it before merge.
0151 verified free across master and every remote branch. 0150 is held by
fix/issue-465, renumbered there from 0143 for the same reason.
Purely a renumber: the document's content is unchanged, and the rename carries
every reference with it (12 @ref annotations in exchange-projector.js, 1 in the
gateway_codex_capture smoke, 14 in codex-exchange-projector.test.js, 1 in
codex-rollout-cwd.test.js, plus the cross-links and Related entries in LLP 0083
and LLP 0141, and the heading). No residual 0144 reference remains on this
branch and all six referenced anchors resolve in the renamed document.
The human directed this renumber explicitly, accepting that moving the head
strips neutral:approved and re-opens the review ladder. It resolves this one
collision and sets no precedent for issue #469, where the general
renumber-versus-qualified-citation convention is still open.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: neutral-reconciler <neutral@example.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: neutral-reconciler <neutral@hyparam.com>
Co-authored-by: neutral-reconciler <neutral-reconciler@users.noreply.github.com>
philcunliffe added a commit that referenced this pull request Jul 30, 2026
…lear the number collision (#486)
Flip LLP 0143-0149 + 0152 (formerly 0142) from Draft to Accepted,
following review and merge of #475.
The steering decision renumbers from 0142 to 0152: a concurrently
authored decision (0142-privacy-surface-and-skill-discoverability,
Accepted) took the same number and already has @ref citations in
shipped skill files, while ours was cited only within this set - so
ours moves. All in-set references updated; 0152 is the next free
number after 0150/0151.
Also rewords 0147's session-correlation note to drop its [inferred]
tag (not-investigated stated plainly; the decision does not depend on
it), per the rule that no [inferred] claim survives into Accepted.
ref-check: the set contributes zero errors; the pre-existing duplicate
numbers 0098/0099/0111 are untouched.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@philcunliffe