feat(gateway): allow chat.inject to override provider attribution via originAgent#79959
feat(gateway): allow chat.inject to override provider attribution via originAgent#79959wherewolf87 wants to merge 5 commits into
Conversation
… originAgent Adds an optional `originAgent` field to ChatInjectParamsSchema. When set, appendInjectedAssistantMessageToTranscript stamps it as the persisted assistant entry's `provider` value, so downstream UIs (avatar/handle resolvers) can correctly attribute cross-agent injects to their actual source (e.g. "hermes", "codex", "claude-code") instead of always showing the OpenClaw avatar. Why: chat.inject is the canonical RPC for cross-agent transcript writes, but the persisted entry currently hard-codes provider="openclaw" with no override path (additionalProperties:false on the schema). When external agents inject via this RPC, the message renders with OpenClaw's avatar and the only visible attribution is whatever they include in the body text. This breaks the "who said what" guarantee in long transcripts. Security note: chat.inject is already gated at operator-privilege scope (method-scopes.ts), so anyone who can call it can already write arbitrary assistant content. originAgent is purely presentational and introduces no new impersonation surface beyond what current callers already have. Backward compat: Field is optional. When absent, behavior is unchanged: provider stamps "openclaw", model stamps "gateway-injected". Files: - src/gateway/protocol/schema/logs-chat.ts (schema field) - src/gateway/server-methods/chat.ts (handler + helper threading) - src/gateway/server-methods/chat-transcript-inject.ts (provider stamp)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb9eb6c32f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Make these explicit so downstream tooling never treats this as model output. | ||
| api: "openai-responses", | ||
| provider: "openclaw", | ||
| provider: params.originAgent ?? "openclaw", |
There was a problem hiding this comment.
Fallback to default provider when originAgent is empty
originAgent is validated as Type.String and then applied via params.originAgent ?? "openclaw", so requests like {"originAgent":""} persist an assistant message with provider: "" instead of the documented default openclaw. This creates malformed attribution records for chat.inject and can break downstream avatar/handle resolution paths that expect a non-empty provider; treat empty/whitespace values as unset before persisting.
Useful? React with 👍 / 👎.
|
Thanks for the contribution. This PR has been inactive for a while and still is not in a landable state. Close this stale, dirty PR: its useful attribution idea was never ported through the active gateway-protocol and transcript-persistence boundaries, so the unchanged May branch is not a viable landing candidate. So I’m closing this as inactive for now. If you pick the work back up, push a rebased branch with green checks and reopen (or open a fresh PR) and it will be reviewed again. Review detailsBest possible solution: If maintainers want caller attribution, define and approve it as an additive gateway-protocol contract, port it through the current persistence and display-projection paths, regenerate clients, and submit a fresh current-main PR with focused coverage. Do we have a high-confidence way to reproduce the issue? Not applicable: this PR proposes an additive gateway attribution capability, not a regression with an established current-main failure path. Is this the best way to solve the issue? No. The branch changes a retired schema location while current Security review: Security review cleared: The branch adds no dependency, workflow, permission, secret, download, or supply-chain surface; its replay-sentinel preservation avoids the earlier transcript-replay regression. AGENTS.md: unclear because the file could not be read completely. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 359859d343bd. |
…Agent
Two committed scripts under scripts/ that exercise the patched code paths
against synthetic transcript fixtures, capturing live runtime output:
scripts/proof-chat-inject-origin-agent.mts
- calls appendInjectedAssistantMessageToTranscript three times with
and without originAgent, reads back the persisted jsonl, prints
the resulting `provider` field for each case
- confirms baseline default ("openclaw") and patched override behavior
scripts/proof-chat-inject-schema.mts
- calls validateChatInjectParams against legacy params, params with
originAgent, and params with an unknown extra field
- confirms originAgent is now accepted while additionalProperties:false
still rejects truly unknown fields
Maintainer can rerun:
tsx scripts/proof-chat-inject-origin-agent.mts
tsx scripts/proof-chat-inject-schema.mts
… field
Addresses Codex P1 + P2 review on this PR.
P1 (correctness): The replay-history filter at
src/agents/pi-embedded-runner/replay-history.ts:271 keys off the pair
(provider="openclaw", model in {"delivery-mirror","gateway-injected"}) to
drop transcript-only injected rows from being replayed back to the next
model call. Overwriting `provider` to the originAgent value broke that
filter, so transcript-only injects could leak into model context.
Fix: keep provider="openclaw" and model="gateway-injected" unchanged on
every injected row. Add `originAgent` as a separate, explicit persisted
field. UI avatar/handle resolvers may key off this new field for display
attribution. The replay filter continues to drop these rows by the
unchanged sentinel pair.
P2 (input hygiene):
- Schema: tighten originAgent to Type.String({ minLength: 1, maxLength: 100 })
so empty literal "" is rejected at validation time.
- Persistence helper: trim whitespace and treat blanks as unset before
writing, defending against any future bypass of the schema check.
Files:
src/gateway/protocol/schema/logs-chat.ts minLength:1
src/gateway/server-methods/chat-transcript-inject.ts
new field, no provider change
apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift
regenerated by pnpm protocol:gen:swift
Proof scripts updated to assert the new contract (provider stays
"openclaw" in every case; originAgent is a separate field that is absent
on baseline, set on attributed calls, and absent on blank input).
oxlint typescript-eslint/no-redundant-type-constituents flagged `string | "absent"` as collapsing to just `string`. Use bare `string`; the "absent" sentinel value is still meaningful at runtime.
…ield Stale JSDoc claimed originAgent stamps the persisted entry's provider field, but the revised design (commit bf61a0c) preserves the provider/model sentinel pair unchanged and persists originAgent as a separate display-attribution field. Doc now matches behavior; also notes that empty/whitespace values are treated as unset. Addresses clawsweeper P3 review feedback on PR openclaw#79959.
|
Review feedback addressed in abbb28f. Finding 1 (Codex P2 — empty/whitespace originAgent): Already addressed in commit Finding 2 (clawsweeper P3 — stale JSDoc): Fixed. JSDoc now correctly states originAgent persists as a separate display-attribution field while the Replay sentinel is intact. Ready for re-review. 🦞 |
|
This pull request has been automatically marked as stale due to inactivity. |
|
This pull request has been automatically marked as stale due to inactivity. |
|
ClawSweeper applied the proposed close for this PR.
|
Summary
Adds an optional
originAgentfield toChatInjectParamsSchemaand persists it as a separate, dedicated attribution field on the injected assistant transcript entry. The pre-existing(provider: "openclaw", model: "gateway-injected")sentinel pair is preserved unchanged, so the runtime replay filter atsrc/agents/pi-embedded-runner/replay-history.ts:271continues to correctly drop these rows from being replayed back to the next model call.UI avatar/handle resolvers can key off the new persisted
originAgentfield to render the true source of a cross-agent inject (e.g. Hermes, Codex) instead of always showing the OpenClaw avatar.Why
chat.injectis the canonical RPC for cross-agent transcript writes. Today the persisted entry hasprovider: "openclaw"hard-coded, withadditionalProperties: falseon the schema closing the door on any caller-supplied attribution. When external agents (Hermes, Codex, Claude Code, etc.) inject via this RPC, the message renders with OpenClaw's avatar and the only visible attribution is whatever the caller embeds in body text. This breaks the "who said what" guarantee in long transcripts.Discovered in production by an external agent (Hermes) attempting a cross-agent verify ping into a webchat session. The body-tag workaround ("FROM: Hermes —") works at-a-glance but degrades attribution clarity over time and across many entries.
Behavior
originAgentset: persisted entry gets a neworiginAgent: "<value>"field.providerandmodelare unchanged.originAgent: behavior is byte-identical to current main. No new field is written. Fully backward compatible.""(minLength: 1); persistence helper additionally trims whitespace and treats blanks as absent, so a malformed value never lands on disk even if a future schema change loosens validation.Why a separate field, not provider override
The Codex review on the prior commit caught a real correctness defect:
provideris part of the runtime replay contract, not just display.normalizeAssistantReplayContent(src/agents/pi-embedded-runner/replay-history.ts:271) drops gateway-injected rows from model replay only whenprovider === "openclaw"and the model is one of{ "delivery-mirror", "gateway-injected" }. Overwritingproviderwould have caused transcript-only injects to leak into model context, duplicating content and risking provider ordering rejections.The redesigned patch keeps the sentinel pair untouched and adds attribution as a dedicated parallel field. UI consumers read the new field; the replay filter is unaffected.
Security
chat.injectis already gated at operator-privilege scope (src/gateway/method-scopes.ts). Anyone who can call it can already write arbitrary assistant content into a session transcript.originAgentis purely presentational and introduces no new impersonation surface beyond what current authorized callers already possess. Because the sentinel pair is preserved, the previously-flagged security risk (UI-only injects entering model replay) does not apply to this revision.Files
src/gateway/protocol/schema/logs-chat.ts— schema field withminLength: 1,maxLength: 100src/gateway/server-methods/chat.ts— handler param + helper threadingsrc/gateway/server-methods/chat-transcript-inject.ts— new persisted field, sentinel preserved, trim+empty normalizationapps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift— regenerated viapnpm protocol:gen:swiftReal behavior proof
Behavior or issue addressed: Cross-agent
chat.injectcalls render with the OpenClaw avatar in the operator's webchat UI because the persisted assistant entry has no caller-controlled attribution field. External agents currently have no way to mark the inject as their own without putting "FROM: Hermes —" inline in body text, which degrades transcript clarity. This patch adds a dedicatedoriginAgentpersisted field while preserving the(provider: "openclaw", model: "gateway-injected")sentinel that the replay-history filter relies on.Real environment tested: Sandboxed checkout of openclaw main at
/tmp/openclaw-pr/openclaw-freshwith the patched files and the regenerated protocol bindings. Used the actual openclaw runtime —appendInjectedAssistantMessageToTranscriptandvalidateChatInjectParamsare invoked vianodethroughtsxagainst synthetic transcript fixtures created with the project's owncreateTranscriptFixtureSynchelper. No mocks, no stubs, no real session keys, no production data.Exact steps or command run after this patch:
Both scripts are committed under
scripts/in this PR so a maintainer can rerun them.Evidence after fix: Live stdout captured from the runtime invocations above. Persistence-layer output shows the actual
provider,model, andoriginAgentfields read back from the jsonl transcript file on disk after the patched code wrote to it. Schema-layer output is the AJV validator's verdict on real input shapes.Persistence-layer terminal output (live runtime, the patched code path actually executed):
Schema-layer terminal output (live AJV validator on patched schema):
Observed result after fix:
originAgent) → persisted entry hasprovider: "openclaw",model: "gateway-injected", nooriginAgentfield. Byte-identical to current-main behavior; replay filter still matches.originAgent: "hermes") → persisted entry hasprovider: "openclaw",model: "gateway-injected", and a neworiginAgent: "hermes"field. Replay filter still matches the sentinel pair and drops the row; UI consumers can attribute it to Hermes via the new field.originAgent: "codex") → same as above withoriginAgent: "codex". Confirms the field is not Hermes-specific.""or whitespace-only) → persisted entry has nooriginAgentfield. The persistence helper trims and normalizes, so malformed input cannot land on disk." hermes ") → persisted entry hasoriginAgent: "hermes"(whitespace-stripped).originAgent, rejects literal""via the newminLength: 1, and continues to reject unknown extra fields via the unchangedadditionalProperties: false.What was not tested: End-to-end UI render in the actual webchat surface (visually confirming a Hermes avatar/handle in the live React UI keyed off the new
originAgentfield) is intentionally out of scope for this PR — the patch only adds the persisted attribution field; UI consumption is a follow-up that will pick up the field from the transcript. The replay-filter regression that the prior PR revision would have introduced is now structurally impossible because the sentinel pair is unchanged; no run-time replay test was added because the contract being preserved already has coverage insrc/agents/pi-embedded-runner/replay-history.test.ts.🤖 Generated with Claude Code