fix(secretrefs): resolve external channel contracts in dist/ sidecars#77421
Conversation
|
Codex review: needs maintainer review before merge. Summary Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path. Next step before merge Review detailsBest possible solution: Retry the Codex review after fixing the execution failure. Do we have a high-confidence way to reproduce the issue? Unclear. The review failed before ClawSweeper could establish a reproduction path. Is this the best way to solve the issue? Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction. What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against a4f2bf273a21. |
42f9ef9 to
26bdd36
Compare
26bdd36 to
b1fcd0f
Compare
|
Maintainer refresh for latest Summary:
Verification:
Pushed refreshed head: |
b1fcd0f to
8e9f5d7
Compare
Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416.
8e9f5d7 to
3b6a20e
Compare
…openclaw#77421) * fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <[email protected]>
…openclaw#77421) * fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <[email protected]>
…openclaw#77421) * fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <[email protected]>
…openclaw#77421) * fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <[email protected]>
…openclaw#77421) * fix(secretrefs): resolve external channel contracts in dist/ sidecars Externalized channel plugins published to npm (e.g. @openclaw/discord since 2026.5.2) keep their compiled secret-contract-api artifact under <rootDir>/dist/, per the package.json `openclaw.runtimeExtensions` convention. The runtime contract loader added in openclaw#76449 only searched the rootDir, so npm-installed plugins silently dropped their channel SecretRef contracts: the runtime snapshot left `channels.<id>.token` as an unresolved SecretRef, the plugin's `isConfigured` check then returned false, and the gateway recorded `error: not configured` without firing the usual channel startup logs. Look in `<rootDir>/dist/` as well as `<rootDir>/`, preferring dist when running from a built openclaw artifact and rootDir when running from source. The new `loads dist/ secret-contract-api sidecars …` test in channel-contract-api.external.test.ts mirrors the real npm-package layout and fails without this change. Refs openclaw#76371. Fixes openclaw#77416. * docs: credit changelog contributor --------- Co-authored-by: Magpie <magpie@local> Co-authored-by: joshavant <[email protected]>
Summary
@openclaw/discordsince 2026.5.2) keep their compiledsecret-contract-apiartifact under<rootDir>/dist/, but the runtime contract loader added in fix(secretrefs): resolve external channel contracts #76449 only searches<rootDir>/. So the loader returnsundefinedfor the real-world npm-package layout, the runtime snapshot leaveschannels.<id>.tokenas an unresolved SecretRef, and channel startup silently bails witherror: not configured.channels.discord.token = { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" }configured (the docs-canonical SecretRef shape). The failure mode just shifted from a loudunresolved SecretRefcrash on 2026.5.2 to a silent skip on 2026.5.3-1 — Discord channel never starts, no error logged in the gateway log. See [Bug] @openclaw/discord 2026.5.3-1 channel still skipped silently with env-backed SecretRef token (regression of #76371; fix #76449 missed dist/ sidecar layout) #77416 for full reproduction.resolvePluginContractApiPathnow also searches<rootDir>/dist/forsecret-contract-api.{js,mjs,cjs,ts,mts,cts}(andcontract-api.*as the secondary basename). Search order prefersdist/when running from a built openclaw artifact (RUNNING_FROM_BUILT_ARTIFACT) and<rootDir>/when running from source, mirroring the existingorderedContractApiExtensionsheuristic. New regression testloads dist/ secret-contract-api sidecars for compiled npm-published external channel pluginsmaterializes a real dist-layout fixture and would fail without the resolver change.openclaw.secretContractApifield added — the dist/ convention is inferred from existingruntimeExtensionsprecedent. Flat-layout external plugins (the existing test fixture at<rootDir>/secret-contract-api.cjs) keep working.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
resolvePluginContractApiPath(rootDir)insrc/secrets/channel-contract-api.ts(added by fix(secretrefs): resolve external channel contracts #76449) only checks<rootDir>/secret-contract-api.{js,mjs,cjs,ts,mts,cts}and<rootDir>/contract-api.{...}. The compiled artifact for npm-published@openclaw/<channel>plugins lives at<rootDir>/dist/secret-contract-api.js(perpackage.jsonopenclaw.runtimeExtensions: ["./dist/index.js"]). Path resolver returnsnull→loadExternalChannelSecretContractFromRecordreturnsundefined→ the channel'scollectRuntimeConfigAssignmentsis never invoked → SecretRef is never queued for resolution → the runtime snapshot leaveschannels.discord.tokenas a SecretRef object →resolveDiscordToken(which usesmode: "inspect"post-fix(secretrefs): resolve external channel contracts #76449) returnstokenStatus: "configured_unavailable"→discordPlugin.config.isConfigured(account)(extensions/discord/src/shared.ts:152: Boolean(account.token?.trim())) returns false → gateway recordslastError: "not configured"and never callsgateway.startAccount, so nochannels/discord :: starting providerlog fires.channel-contract-api.external.test.tsfixture writes the contract module at<rootDir>/secret-contract-api.cjs(flat layout). Real npm packages bundle compiled artifacts underdist/. No test exercised the dist-layout case, so fix(secretrefs): resolve external channel contracts #76449 went green even though it would not pick up a real install.@openclaw/discord, the contract loader was generalized correctly except for the artifact-location convention. Bundled plugins use a different loader (loadBundledPluginPublicArtifactModuleSync) that already knows about layout, so this only manifests for npm-installed externalized channels.Regression Test Plan (if applicable)
src/secrets/channel-contract-api.external.test.ts(added a newit("loads dist/ secret-contract-api sidecars for compiled npm-published external channel plugins", ...)).rootDiris structured like a published npm package (compiled artifacts underdist/),loadChannelSecretContractApifinds and returns thesecret-contract-apimodule so the SecretRef resolution path can run.makeTrackedTempDir+loadPluginMetadataSnapshotMockmachinery as the existing flat-layout test, just writes the file underdist/. No live Discord/Telegram credentials, no end-to-end gateway boot needed.loads root secret-contract-api sidecars for external channel plugins) explicitly writes to<rootDir>/secret-contract-api.cjs, not<rootDir>/dist/.User-visible / Behavior Changes
Channel SecretRefs that round-trip through the runtime snapshot for npm-installed externalized channel plugins (currently
@openclaw/discord; same shape applies to any future externalized channel) actually resolve again. Before this fix,channels.<channel>.token = { source: "env", provider: "default", id: "..." }(the docs-canonical shape) silently failed at gateway start on 2026.5.3-1; the only working configuration was to remove the SecretRef and rely on the plugin's env-fallback. After this fix, the documented SecretRef config works end-to-end again.No config migration. No defaults change. No new fields.
Diagram (if applicable)
Security Impact (required)
openBoundaryFileSyncboundary check remains unchanged:rootPath: record.rootDir,rejectHardlinks: record.origin !== "bundled",boundaryLabel: "plugin root". The added<rootDir>/dist/candidate is strictly inside the same root boundary, so the boundary file checks still apply.Yes, explain risk + mitigation: N/A.Repro + Verification
Environment
@openclaw/discord2026.5.3 npm install){ "secrets": { "providers": { "default": { "source": "env" } } }, "channels": { "discord": { "enabled": true, "token": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" } } } }DISCORD_BOT_TOKENautoloaded from~/.openclaw/.envinto the gateway'sprocess.env.Steps
[email protected](openclaw updatefrom a 2026.5.2 install). Plugin sync installs@openclaw/[email protected]at~/.openclaw/npm/node_modules/@openclaw/discord/(compiled artifacts indist/).channels.discord.tokenSecretRef config above. Restart gateway:launchctl kickstart -k gui/$UID/ai.openclaw.gateway./tmp/openclaw/openclaw-<date>.logand runopenclaw channels status. Observe absentchannels/discord :: [default] starting providerline andDiscord default: ... stopped, disconnected, ... error:not configured.pnpm vitest run src/secrets/channel-contract-api.external.test.ts).Expected
loadChannelSecretContractApi({channelId: "discord", ...})returns the contract module from<rootDir>/dist/secret-contract-api.js.channels/discord :: [default] starting provider (@<bot>). Channel status reportsrunning, connected.Actual
loads dist/ secret-contract-api sidecars …) fails onmain:expect(api?.secretTargetRegistryEntries).toEqual(...)receivesundefinedbecauseloadExternalChannelSecretContractFromRecordreturns nothing without the dist-search.pnpm vitest run src/secrets/channel-contract-api.external.test.ts src/secrets/runtime-external-channel-origin-discovery.test.ts src/secrets/runtime-config-collectors-channels.test.ts extensions/discord/src/setup-account-state.test.ts extensions/discord/src/account-inspect.test.ts extensions/discord/src/token.test.ts— all 25 tests across 6 files green.<rootDir>/dist/secret-contract-api.jsexists, root-level does not).Evidence
Tests 1 failed | 2 passed (3). With this commit:Tests 3 passed (3).Human Verification (required)
Received: undefinedonsecretTargetRegistryEntries.channel-contract-api.external.test.tstests + the related runtime-external-channel-origin-discovery / runtime-config-collectors-channels / discord setup-account-state / discord account-inspect / discord token tests. All green.2026.5.3-1install that the actual sidecar lives at~/.openclaw/npm/node_modules/@openclaw/discord/dist/secret-contract-api.jsand that no root-level sibling exists.RUNNING_FROM_BUILT_ARTIFACT === false) still searches<rootDir>/first so an in-tree TS source plugin would still match itssecret-contract-api.tsahead of anydist/mirror.<rootDir>/dist/first, matching production reality for npm-installed plugins.secret-contract-apiahead ofcontract-api(the legacy basename).openBoundaryFileSyncboundary check: the addeddist/candidate is strictly insiderecord.rootDir, so the existingrootPath: record.rootDirboundary check still applies. No change to hardlink rejection (rejectHardlinks: record.origin !== "bundled") or root-escape semantics.loads root secret-contract-api sidecars …) still passes — the fix is additive, not a replacement.openclawpackage on my host (deferred until review accepts the diff). Targeted CI lanes beyond the listed test files (broaderpnpm testlane, tsgo, lint).Review Conversations
Compatibility / Migration
Risks and Mitigations
<rootDir>/secret-contract-api.{ext}AND a stale<rootDir>/dist/secret-contract-api.{ext}(unusual) may see different precedence depending on whether openclaw was run from source vs. built artifact.RUNNING_FROM_BUILT_ARTIFACTprecedence pattern inorderedContractApiExtensions. Documented in the in-file comment. Real published packages have only one location.dist/directory (e.g.lib/orout/).dist/is the convention used by every existing externalized channel plugin perpackage.jsonopenclaw.runtimeExtensions: ["./dist/..."]. If a future plugin needs a different convention, an explicitpackage.jsonopenclaw.secretContractEntryfield (similar to the existingruntimeSetupEntry) would be the right way to support it. Filed under "future work, not now."AI-assisted disclosure
This PR was drafted with AI assistance (Claude Code, model
claude-opus-4-7-1m). The author independently verified: the on-disk plugin layout at~/.openclaw/npm/node_modules/@openclaw/discord/, the production reproduction (channel silently fails with SecretRef, comes up with env-fallback), the failing-then-passing test, and the targeted test suite. Lightly tested locally (focused vitest lanes); broaderpnpm check/pnpm testlanes and the CI matrix are deferred to GitHub Actions.Session origin: filed via #77416 from a real production 2026.5.3-1 install hitting the silent-skip bug. The bug was confirmed by toggling the SecretRef config and observing the channel status flip between
running, connected, bot:@<botname>(env-fallback) andstopped, disconnected, error:not configured(SecretRef path). Source diagnosis traced throughextensions/discord/src/token.ts:resolveDiscordToken,accounts.ts:resolveDiscordAccount,shared.ts:isConfigured,src/gateway/server-channels.ts:startChannelInternal(line 442-458 gating onisConfigured), andsrc/secrets/channel-contract-api.ts:resolvePluginContractApiPath. Final clue: the existing test fixture (channel-contract-api.external.test.ts:31) writes a flat-layout sidecar that doesn't match real npm-package layout, masking the dist/ resolver gap.