Skip to content

fix(reply): resolve active channel/account SecretRefs in reply runs#66796

Merged
joshavant merged 3 commits into
mainfrom
fix/reply-active-channel-secret-resolution
Apr 14, 2026
Merged

fix(reply): resolve active channel/account SecretRefs in reply runs#66796
joshavant merged 3 commits into
mainfrom
fix/reply-active-channel-secret-resolution

Conversation

@joshavant

Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: reply-run secret resolution only requested base agent runtime targets, so channels.* SecretRefs (for the active reply channel) could remain unresolved during agent tool discovery.
  • Why it matters: Discord message-action discovery can throw on unresolved token SecretRefs, causing noisy [message-action-discovery] errors and degraded/incomplete turns even when channel status/probe looks healthy.
  • What changed: reply execution now performs a second, scoped channel/account SecretRef resolution pass for the active reply context (originatingChannel > messageProvider, originatingAccountId > agentAccountId).
  • What did NOT change (scope boundary): no global expansion of agent runtime secret target scope; no feature flags; no plugin-specific behavior changes.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #N/A
  • Related #N/A
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveQueuedReplyExecutionConfig only resolved getAgentRuntimeCommandSecretTargetIds() targets. By default that excludes channels.*, so active channel SecretRefs could remain raw in reply runs.
  • Missing detection / guardrail: no reply-runtime test asserted that active channel/account SecretRefs are resolved before message tool schema/action discovery.
  • Contributing context (if known): Discord token resolution intentionally fails closed on unresolved SecretRefs; message-action discovery catches/logs that failure.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/auto-reply/reply/agent-runner-utils.secret-resolution.test.ts
    • src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts
    • src/auto-reply/reply/followup-runner.test.ts
  • Scenario the test should lock in:
    • reply runtime resolves base targets and then active channel/account targets (scoped) before tool discovery paths consume config.
  • Why this is the smallest reliable guardrail:
    • it validates the exact resolver seam and callsites used by both direct and queued reply paths without broad unrelated runtime dependencies.
  • Existing test that already covers this (if any):
    • extensions/discord/src/token.test.ts already asserts unresolved Discord SecretRefs throw in strict token resolution.
  • If no new test is added, why not:
    • N/A (new tests added).

User-visible / Behavior Changes

  • Active reply channel/account SecretRefs are resolved before reply-run tool discovery.
  • If the active reply channel/account SecretRef is unresolved, reply runs fail earlier and explicitly (hard-fail), instead of degrading into discovery-time errors.

Diagram (if applicable)

Before:
reply run -> resolve base runtime secrets only -> message tool discovery -> discord token SecretRef still raw -> discovery error log

After:
reply run -> resolve base runtime secrets -> resolve active channel/account secrets (scoped) -> message tool discovery uses resolved token -> no unresolved SecretRef discovery error

Security Impact (required)

  • New permissions/capabilities? (Yes/No)
    • No
  • Secrets/tokens handling changed? (Yes/No)
    • Yes
  • New/changed network calls? (Yes/No)
    • No
  • Command/tool execution surface changed? (Yes/No)
    • No
  • Data access scope changed? (Yes/No)
    • No
  • If any Yes, explain risk + mitigation:
    • Secret-resolution behavior changed for reply runs: active channel/account scopes are now resolved strictly before execution. Mitigated by scoped target/allowedPaths selection and regression tests.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node.js v24.14.1, pnpm
  • Model/provider: OpenAI gpt-5.4 (invalid API key used intentionally to end run quickly)
  • Integration/channel (if any): Discord
  • Relevant config (redacted): channels.discord.token configured as env:default:DISCORD_BOT_TOKEN SecretRef

Steps

  1. Configure Discord token as SecretRef object in runtime config.
  2. Run embedded reply path with active Discord context without scoped reply secret resolution (control).
  3. Run same path with resolveQueuedReplyExecutionConfig(...) scoped channel/account resolution first.

Expected

  • Control: unresolved SecretRef discovery error is logged.
  • Fixed path: no unresolved SecretRef discovery error for Discord message-action discovery.

Actual

  • Control produced:
    • [message-action-discovery] discord.actions.describeMessageTool failed: ... unresolved SecretRef "env:default:DISCORD_BOT_TOKEN"
  • Fixed path produced:
    • resolved Discord token type became string
    • zero unresolved discovery errors captured

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • direct reply path now passes active scope into resolveQueuedReplyExecutionConfig
    • queued followup path now passes active scope into resolveQueuedReplyExecutionConfig
    • scoped channel/account resolver pass executes and applies allowedPaths when account is known
    • manual control-vs-fixed repro for Discord SecretRef discovery behavior
  • Edge cases checked:
    • missing active channel skips scoped pass
    • fallback to messageProvider/agentAccountId when originating values are missing
    • runtime snapshot remains preferred base config input
  • What you did not verify:
    • full live Discord gateway send roundtrip with production credentials

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No)
    • Yes
  • Config/env changes? (Yes/No)
    • No
  • Migration needed? (Yes/No)
    • No
  • If yes, exact upgrade steps:
    • N/A

Risks and Mitigations

  • Risk:
    • Runs can fail earlier for unresolved active channel/account SecretRefs that previously degraded later.
    • Mitigation:
      • this is intentional hard-fail behavior for active reply routing correctness; scope is limited to active channel/account only.
  • Risk:
    • Future changes could accidentally broaden scoped resolution.
    • Mitigation:
      • dedicated scoped-resolution tests assert channel/account targeting and no-op behavior when no active channel is resolved.

@aisle-research-bot

aisle-research-bot Bot commented Apr 14, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Global runtime config snapshot used for queued reply secret resolution (cross-tenant/config confusion)
2 🟠 High allowedPaths not enforced on gateway secret assignments (scoped secret resolution bypass)
3 🟠 High Channel-scoped secret resolution may disclose other accounts' secrets when accountId is missing (allowedPaths omitted)
1. 🟠 Global runtime config snapshot used for queued reply secret resolution (cross-tenant/config confusion)
Property Value
Severity High
CWE CWE-488
Location src/auto-reply/reply/agent-runner-utils.ts:31-52

Description

resolveQueuedReplyExecutionConfig() now prefers a process-global runtime config snapshot (getRuntimeConfigSnapshot()) as the base configuration for resolving command secret references. Because the snapshot is implemented as a module-level singleton (no keying by session/account/tenant) in src/config/runtime-snapshot.ts, a queued run can resolve secrets against whatever snapshot was most recently set.

This creates a realistic cross-context information disclosure risk when:

  • The process handles multiple accounts/tenants/workspaces (or multiple concurrent sessions with different effective configs)
  • setRuntimeConfigSnapshot() is called by one request/session/config write
  • A different queued followup later executes and calls resolveQueuedReplyExecutionConfig()

Impact is increased by the newly added second-pass scoped secret resolution, which may resolve channel/account-scoped secrets (e.g., channels.<provider>.accounts.<accountId>.*) from the wrong runtime snapshot.

Vulnerable behavior:

  • Input: queued run provides config and originating channel/account params
  • Global mutable state: getRuntimeConfigSnapshot()
  • Sink: resolveCommandSecretRefsViaGateway({ config: runtimeConfig, ... }) resolves secret refs for that (possibly wrong) config

Vulnerable code:

export function resolveQueuedReplyRuntimeConfig(config: OpenClawConfig): OpenClawConfig {
  return (
    (typeof getRuntimeConfigSnapshot === "function" ? getRuntimeConfigSnapshot() : null) ?? config
  );
}// ... later used as base for secret resolution
const runtimeConfig = resolveQueuedReplyRuntimeConfig(config);
const { resolvedConfig } = await resolveCommandSecretRefsViaGateway({
  config: runtimeConfig,
  commandName: "reply",
  targetIds: getAgentRuntimeCommandSecretTargetIds(),
});

Recommendation

Avoid using a process-global, mutable runtime snapshot as the implicit base for queued execution secret resolution.

Recommended fixes (pick one):

  1. Key runtime snapshots by tenant/session/account (and pass the key through the queue), e.g. via sessionKey/accountId, or use AsyncLocalStorage to bind snapshot to the current request context.

  2. Persist an immutable config snapshot with the queued job and always resolve secrets against that snapshot.

  3. If a global snapshot must exist, verify provenance before using it (e.g., compare snapshot hash / configPath / tenant id) and fall back to the job’s provided config if it doesn’t match.

Example (conceptual) approach using an explicit snapshot key:

// when enqueuing
queueItem.runtimeConfigKey = { tenantId, configHash };// when executing
const runtimeConfig = getRuntimeConfigSnapshot(queueItem.runtimeConfigKey) ?? queueItem.config;

Also consider guarding the new scoped resolution so it only ever resolves within allowed tenant/account boundaries derived from the queued job (not from global state).

2. 🟠 allowedPaths not enforced on gateway secret assignments (scoped secret resolution bypass)
Property Value
Severity High
CWE CWE-285
Location src/cli/command-secret-gateway.ts:743-759

Description

resolveCommandSecretRefsViaGateway accepts an allowedPaths set intended to restrict which secret paths may be resolved (e.g., to a specific channel account). However, when the gateway returns resolved secret assignments, the client applies all returned assignments to the config without checking allowedPaths.

This means that if the gateway returns assignments for additional secret paths (for example other channels.<channel>.accounts.<otherAccountId>.* entries) those values will still be written into the runtime config, bypassing the intended scoping/authorization.

Why this matters in the new change:

  • New code paths (e.g., resolveQueuedReplyExecutionConfig) now compute allowedPaths via getScopedChannelsCommandSecretTargets for a particular channel/accountId.
  • But because allowedPaths is not enforced on gateway responses, a caller that can influence the secret-resolution scope (channel/accountId) or a permissive/misbehaving gateway can cause unrelated secrets to be injected into the resolved config.

Vulnerable code:

for (const assignment of parsed.assignments) {
  ...
  setPathExistingStrict(resolvedConfig, pathSegments, assignment.value);
}

No allowedPaths check is performed before applying the assignment.

Recommendation

Enforce allowedPaths before applying gateway assignments.

For example:

for (const assignment of parsed.assignments) {
  const pathSegments = assignment.pathSegments.filter((s) => s.length > 0);
  if (pathSegments.length === 0) continue;
  const path = pathSegments.join(".");

  if (params.allowedPaths && !params.allowedPaths.has(path)) {
    continue; // or record a diagnostic / throw
  }

  setPathExistingStrict(resolvedConfig, pathSegments, assignment.value);
}

Additionally, consider sending allowedPaths (or an equivalent constraint) to the gateway API so the gateway only resolves/returns permitted paths.

3. 🟠 Channel-scoped secret resolution may disclose other accounts' secrets when accountId is missing (allowedPaths omitted)
Property Value
Severity High
CWE CWE-284
Location src/auto-reply/reply/agent-runner-utils.ts:64-78

Description

resolveQueuedReplyExecutionConfig performs a second, channel-scoped secret resolution pass. When a channel is known but the accountId is not, getScopedChannelsCommandSecretTargets returns targetIds without allowedPaths. Because allowedPaths is then conditionally spread into the gateway call, the scoped resolution becomes unrestricted within the channel target IDs, including channels.<channel>.accounts.<any>.*.

Impact:

  • A queued reply execution that only has a channel context (no account context) may resolve secrets for all accounts under that channel, potentially disclosing credentials/tokens belonging to other accounts.

Why it happens:

  • getScopedChannelsCommandSecretTargets returns { targetIds } when !channel || !normalizedAccountId.
  • resolveQueuedReplyExecutionConfig checks only scope.channel before running the scoped resolution.
  • resolveCommandSecretRefsViaGateway enforces path restrictions only when allowedPaths is provided; when missing, all discovered targets for those IDs are eligible.

Vulnerable code:

const scopedTargets = getScopedChannelsCommandSecretTargets({
  config: baseResolvedConfig,
  channel: scope.channel,
  accountId: scope.accountId,
});

const scopedResolved = await resolveCommandSecretRefsViaGateway({
  config: baseResolvedConfig,
  commandName: "reply",
  targetIds: scopedTargets.targetIds,
  ...(scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {}),
});

Recommendation

Treat allowedPaths as an enforcement boundary and ensure it is always applied for scoped resolution.

Options:

  1. Require accountId for any scoped resolution that includes account-level targets:
if (!scope.channel || !scope.accountId) return baseResolvedConfig;
  1. Always provide allowedPaths from getScopedChannelsCommandSecretTargets even when accountId is missing, limited to non-account channel secrets (exclude channels.<channel>.accounts.*). For example, update getScopedChannelsCommandSecretTargets to compute allowedPaths for channel scope:
// if channel is set but accountId is not, allow only channel-level paths
if (channel && !normalizedAccountId) {
  const allowedPaths = new Set<string>();
  for (const target of discoverConfigSecretTargetsByIds(params.config, targetIds)) {
    const [root, channelId, maybeAccounts] = target.pathSegments;
    if (root === "channels" && channelId === channel && maybeAccounts !== "accounts") {
      allowedPaths.add(target.path);
    }
  }
  return { targetIds, allowedPaths };
}
  1. In resolveQueuedReplyExecutionConfig, pass an empty set to force-deny when scoping info is incomplete:
allowedPaths: scopedTargets.allowedPaths ?? new Set(),

Any of these ensures scoped resolution cannot expand to other accounts’ secrets when only a channel is known.


Analyzed PR: #66796 at commit 7f4f01a

Last updated on: 2026-04-14T21:10:06Z

@openclaw-barnacle openclaw-barnacle Bot added size: M maintainer Maintainer-authored PR labels Apr 14, 2026
@greptile-apps

greptile-apps Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where resolveQueuedReplyExecutionConfig only resolved base agent runtime secret targets (via getAgentRuntimeCommandSecretTargetIds), which excluded channels.* entries. As a result, active reply channel SecretRefs (e.g., Discord's channels.discord.token) could remain raw during message-action discovery, causing noisy [message-action-discovery] errors.

The fix adds a second, scoped secret resolution pass keyed on the active channel/account (originatingChannelmessageProvider, originatingAccountIdagentAccountId) with allowedPaths narrowing when an account is known. Both the direct reply path in agent-runner.ts and the queued path in followup-runner.ts are updated, and the new agent-runner-utils.secret-resolution.test.ts file provides thorough coverage of the two-pass resolution logic.

Confidence Score: 5/5

Safe to merge — the fix is correctly scoped, backward-compatible, and fully covered by new regression tests.

All changes are targeted and additive: the optional params argument keeps the existing call sites backward-compatible, both direct and queued reply paths are updated consistently, the two-pass resolution logic handles all edge cases (missing channel, empty targetIds, runtime snapshot preference), and the new test file locks in each branch. No P0 or P1 issues found.

No files require special attention.

Reviews (1): Last reviewed commit: "Reply: resolve active channel/account Se..." | Re-trigger Greptile

@joshavant joshavant self-assigned this Apr 14, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaa5b3d84c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +138 to +142
queued.run.config = await resolveQueuedReplyExecutionConfig(queued.run.config, {
originatingChannel: queued.originatingChannel,
messageProvider: queued.run.messageProvider,
originatingAccountId: queued.originatingAccountId,
agentAccountId: queued.run.agentAccountId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep scoped secret resolution in queued followup execution

This new scoped call can return a freshly resolved config object, but in queued runs that value is immediately superseded by resolveQueuedReplyRuntimeConfig(...) when a runtime snapshot is present, so the channel/account SecretRefs resolved here are dropped before runEmbeddedPiAgent executes. In the common case where the snapshot has base runtime secrets resolved but channel secrets still as SecretRefs, queued followups will continue using unresolved channel config (the exact regression this commit aims to fix), so message-action discovery can still fail for channels like Discord.

Useful? React with 👍 / 👎.

@joshavant joshavant force-pushed the fix/reply-active-channel-secret-resolution branch from aaa5b3d to 7f4f01a Compare April 14, 2026 21:04
@joshavant joshavant merged commit 731d466 into main Apr 14, 2026
2 checks passed
@joshavant joshavant deleted the fix/reply-active-channel-secret-resolution branch April 14, 2026 21:05
@joshavant

Copy link
Copy Markdown
Contributor Author

Landed via temp rebase onto main.

  • Gate: pnpm lint && pnpm build && pnpm test
  • Land commit: 7f4f01a
  • Merge commit: MERGE_SHA

Thanks @CONTRIB!

@joshavant

Copy link
Copy Markdown
Contributor Author

Correction (resolved placeholders):

Landed via temp rebase onto main.

  • Gate: pnpm lint && pnpm build && pnpm test
  • Land commit: 7f4f01a
  • Merge commit: 731d466

Thanks @joshavant!

@joshavant

Copy link
Copy Markdown
Contributor Author

Verification note: the full pnpm lint && pnpm build && pnpm test command was executed locally. lint and build passed; test reported existing unrelated failures on current main baseline (outside this PR surface). Scoped reply-run regression tests for touched files passed:

  • pnpm test src/auto-reply/reply/agent-runner-utils.secret-resolution.test.ts src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts src/auto-reply/reply/followup-runner.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f4f01a070

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


return async (queued: FollowupRun) => {
queued.run.config = await resolveQueuedReplyExecutionConfig(queued.run.config);
queued.run.config = await resolveQueuedReplyExecutionConfig(queued.run.config, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve scoped secret resolution in queued followup runs

resolveQueuedReplyExecutionConfig(...) now computes scoped channel/account secret resolution and assigns it to queued.run.config, but in the queued runner this value is still superseded immediately afterward by resolveQueuedReplyRuntimeConfig(...) when a runtime snapshot exists, so runEmbeddedPiAgent continues with snapshot config and can miss the newly resolved channel/account secrets (for example, Discord token SecretRefs during message-action discovery). Fresh evidence in this commit: the new scoped call is present here, and the subsequent snapshot re-read path remains unchanged, so queued followups still drop the scoped result.

Useful? React with 👍 / 👎.

kvnkho pushed a commit to kvnkho/openclaw that referenced this pull request Apr 17, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…penclaw#66796)

* Reply: resolve active channel/account SecretRefs in agent runs

* tests(reply): assert queued config scope wiring

* fix: document reply secret-scope regression coverage (openclaw#66796) (thanks @joshavant)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant