fix(feishu): resolve exec/keychain appSecret SecretRefs on the outbound path#89454
fix(feishu): resolve exec/keychain appSecret SecretRefs on the outbound path#89454openperf wants to merge 4 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 9:06 PM ET / 01:06 UTC. Summary PR surface: Source +113, Tests +220. Total +333 across 8 files. Reproducibility: yes. at source level. Current main documents Feishu appSecret as a SecretRef surface, the strict Feishu runtime resolver throws on unresolved non-env SecretRefs, and media/reply paths still pass cfg into that strict path; I did not run a live Feishu tenant send. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Refresh the branch, then either land this scoped Feishu repair after Feishu/SecretRef owners accept send-time credential resolution or move the invariant to a single Feishu client/runtime-snapshot boundary. Do we have a high-confidence way to reproduce the issue? Yes at source level. Current main documents Feishu appSecret as a SecretRef surface, the strict Feishu runtime resolver throws on unresolved non-env SecretRefs, and media/reply paths still pass cfg into that strict path; I did not run a live Feishu tenant send. Is this the best way to solve the issue? Unclear as the final architecture, but acceptable as a bounded repair if owners accept the credential boundary. The cleaner long-term shape is resolving app credentials at one Feishu client/runtime-snapshot boundary rather than from several outbound entry points. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 54b09580f61b. Label changesLabel justifications:
Evidence reviewedPR surface: Source +113, Tests +220. Total +333 across 8 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (1 earlier review cycle)
|
|
This looks like a solid seam for fixing #89338 without widening the scope too much. One small question: for the implicit/top-level account case, |
5166fc6 to
c8c4172
Compare
|
@byungskers Not intentional, just an oversight. That |
c8c4172 to
1981e05
Compare
…nd path The strict sync Feishu runtime resolver throws FeishuSecretRefUnavailableError on an unresolved exec/keychain appId/appSecret SecretRef before the SDK client is built, dropping outbound and message-tool sends (issue openclaw#89338). Add resolveFeishuOutboundCredentialsConfig, which inlines a still-unresolved app credential via the async exec/keychain-capable SDK resolver into an in-memory cfg copy before the strict resolver runs. Wired into the outbound adapter (sendPayload/sendText/sendMedia) and the message-tool handleAction (card/media/ text). Resolution writes back at the matched, case-preserving account-map key, or top-level for an implicit/inherited account, preserving other per-account settings; the diagnostic path mirrors that write-back location. Plaintext / already-resolved configs are returned unchanged (no resolver call, no clone). Fixes openclaw#89338
9c90c2d to
6f799e5
Compare
|
This pull request has been automatically marked as stale due to inactivity. |
Summary
appSecretis configured as aSecretRefbacked by anexec/Keychain provider. Normal chat (WebSocket connection) and text replies keep working; only the media upload is dropped, so the recipient receives no image. The regression appeared after migrating credentials from plaintext to a Keychain-backedSecretRef.resolveFeishuRuntimeAccount→resolveFeishuSecretLike, a synchronous resolver that only understands plaintext strings andenv-source SecretRefs; for anexec/file/KeychainSecretRefit throwsFeishuSecretRefUnavailableError. The runtime config (getRuntimeConfig→loadConfig) carries SecretRefs unresolved — they are resolved on demand, not inlined globally — so every path that re-runs this strict resolver against anexec/KeychainappSecretthrows. An already-established WebSocket session keeps working because its Lark client and tenant token are built once and reused, with no per-message re-resolution; but each media/outbound send re-runs the per-send strict resolution: it throws, the throw is caught infeishuOutbound.sendMedia(and the local-image path insendText), and the send degrades to a plain URL/text fallback — the "silent" failure the reporter saw. Plaintext worked because a plaintext value is returned immediately, bypassing SecretRef resolution entirely.resolveFeishuOutboundCredentialsConfiginspects the outbound account'sappId/appSecret; if either is still aSecretRef, it resolves it through the async, exec/Keychain-capable SDK resolver (resolveConfiguredSecretInputString) — the sync runtime resolver only handles env/plaintext, which is the gap. The resolved string is inlined into a shallow copy of the config (at the account level when an accounts map exists, top-level otherwise). The threefeishuOutboundentry points (sendPayload,sendText,sendMedia), the agent message-toolsend/thread-replyaction handler (channel.ts), and the source-channel reply dispatcher (createFeishuReplyDispatcherinreply-dispatcher.ts) all call it first, so the existing synchronous downstream resolution now sees a resolved string and the send proceeds.extensions/feishu/src/accounts.ts— addresolveFeishuOutboundCredentialsConfig(and a smallresolveFeishuCredentialRefValuehelper) that resolves unresolvedappId/appSecretSecretRefs through the asyncopenclaw/plugin-sdk/secret-input-runtimeresolver and inlines them into the returned config at the existing matched account-map key.extensions/feishu/src/outbound.ts— resolve credentials at the start of thefeishuOutboundsendPayload,sendText, andsendMediahandlers before any SDK client is built.extensions/feishu/src/channel.ts— resolve credentials once at the start of the message-toolsend/thread-replyaction handler and use the resolved config for the card, media, and text branches.extensions/feishu/src/reply-dispatcher.ts— resolve credentials lazily (memoized per dispatcher instance) via agetResolvedCfg()helper; all sixsendMediaFeishu,sendMessageFeishu, andsendStructuredCardFeishucall sites withincreateFeishuReplyDispatchernow use the resolved config. This covers the source-channel MEDIA/final-reply path that ClawSweeper identified as uncovered (P1 finding).extensions/feishu/src/accounts.test.ts/channel.test.ts/reply-dispatcher.test.ts— regression cases: the strict runtime account throws on an exec/KeychainappSecretSecretRef and succeeds after the helper inlines it; a MEDIA payload viacreateFeishuReplyDispatcherwith an exec SecretRef reachessendMediaFeishuwith the resolved config; plaintext credentials return the same config object unchanged.docs/reference/config); the on-diskSecretRefshape is read, never rewritten.extensions/*api/runtime-api, registry/loader); the helper only consumes an existing public SDK subpath (openclaw/plugin-sdk/secret-input-runtime), already used by other bundled plugins.Reproduction
appSecretas anexec/KeychainSecretRef, e.g.channels.feishu.accounts.<id>.appSecret = { "source": "exec", "provider": "keychain_feishu_<id>", "id": "value" }.MEDIAdirective in a Feishu DM.FeishuSecretRefUnavailableError, is caught, and degrades to a URL/text fallback — no image is delivered.Deterministic source reproduction (added as regression tests): with the reporter's exact
execSecretRef shape,resolveFeishuRuntimeAccountthrows; afterresolveFeishuOutboundCredentialsConfig, the same call resolves with the inlined secret. The same pattern is verified for the source-channel reply-dispatcher MEDIA path.Real behavior proof
Behavior addressed (#89338 — Feishu media upload now works with an exec/Keychain SecretRef appSecret): outbound delivery resolves unresolved app-credential SecretRefs before building the Feishu client, instead of dropping the upload to a URL/text fallback. This now covers both the outbound/message-tool path and the source-channel reply dispatcher path (
createFeishuReplyDispatcher).Real environment tested (Linux, Node — Vitest against the real
extensions/feishu/src/accounts.tsresolution path, with the SDK secret resolver mocked to emulate the gateway secret runtime; no live Feishu tenant): verified viaaccounts.test.ts,channel.test.ts,outbound.test.ts, andreply-dispatcher.test.ts, plus extension production and test type-checks and a format check on the changed files. A live Feishu media send against a real tenant was not run.Exact steps or command run after this patch (repo-root):
node scripts/run-vitest.mjs extensions/feishu/src/{accounts,channel,outbound,reply-dispatcher}.test.ts;node scripts/run-tsgo.mjs -p tsconfig.extensions.jsonandnode scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json;pnpm oxfmt --checkandnode scripts/run-oxlint.mjson the changed files.Evidence after fix (Vitest output for the touched test files):
The cases prove the regression and its fix on the real resolver: the strict runtime account throws
FeishuSecretRefUnavailableErroron an exec/KeychainappSecretSecretRef; afterresolveFeishuOutboundCredentialsConfiginlines it, the same call resolves successfully. The reply-dispatcher regression test proves a MEDIA payload delivered throughcreateFeishuReplyDispatcherwith an exec SecretRef cfg reachessendMediaFeishuwith the resolved config (not the original SecretRef cfg). Plaintext credentials return the identical config object (no clone, no resolver call).Observed result after fix (all delivery paths):
resolveFeishuRuntimeAccounton the resolved config returnsconfigured: truewith the resolvedappSecret, so the SDK client builds and the upload proceeds. Extension prod + test type-checks pass; oxlint and oxfmt are clean.What was not tested (live Feishu tenant media send): a real Feishu DM image send over a live WebSocket session with a Keychain-backed secret was not reproduced; the proof is the deterministic module-level reproduction the issue describes.
Risk / Mitigation
appId/appSecretis still aSecretRef; plaintext and already-resolved configs short-circuit with no resolver call and no clone, so the common path is unchanged. When a credential is genuinely unresolvable, the helper leaves the config untouched and the existing transient-error fallback still applies.SecretRef). Mitigation: bounded to the unresolved case — plaintext / already-resolved sends short-circuit — and it is the same resolution the strict resolver would otherwise attempt and throw on. The reply-dispatcher path memoizes the resolution per dispatcher instance (resolved once on first send, reused for all subsequent sends in that reply context).Out of scope / known siblings
resolveFeishuRuntimeAccountoutside the four entry points covered here (card-action callbacks, comment replies, pins, bitable, chat, directory lookups, monitoring) would still hitFeishuSecretRefUnavailableErrorwith a config carrying an unresolved exec/Keychain SecretRef. They are not part of the reported regression. The structurally complete fix would move resolution to the client-build boundary (createFeishuClient); that is a deliberate separate refactor.exec/keychainSecretRefs — is not necessarily unique to Feishu. This PR intentionally scopes to the reported Feishu path.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Fixes #89338