Skip to content

Infra: require explicit opt-in for prerelease npm installs#38117

Merged
vincentkoc merged 10 commits intomainfrom
vincentkoc-code/npm-install-prerelease-opt-in
Mar 6, 2026
Merged

Infra: require explicit opt-in for prerelease npm installs#38117
vincentkoc merged 10 commits intomainfrom
vincentkoc-code/npm-install-prerelease-opt-in

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

  • Problem: openclaw plugins install <pkg> and openclaw hooks install <pkg> could accept prerelease resolutions when npm returned a beta/rc for a bare spec or @latest.
  • Why it matters: stable-by-default installs should not silently move users onto prerelease builds, and semver ranges were being accepted even though the docs already described a tighter contract.
  • What changed: shared npm spec parsing now only accepts bare names, exact versions, or dist-tags; bare specs and @latest reject prerelease resolutions unless the user explicitly opts in with a prerelease tag or exact prerelease version; tests and docs were updated for plugins and hooks.
  • What did NOT change (scope boundary): explicit prerelease installs like @beta, @rc, or @1.2.3-beta.4 still work, and this does not change global OpenClaw update-channel behavior.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • 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 #
  • Related #

User-visible / Behavior Changes

  • Bare plugin/hook npm specs and @latest now stay stable-only and fail if npm resolves them to a prerelease.
  • Semver ranges like ^1.2.3 and ~1.2.3 are now rejected for plugin/hook npm installs.
  • Users must explicitly opt into prereleases with a prerelease dist-tag or exact prerelease version.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): plugins + hooks CLI
  • Relevant config (redacted): N/A

Steps

  1. Mock npm pack to resolve a bare plugin or hook spec to a prerelease version.
  2. Run the install flow for a bare spec / @latest and for an explicit prerelease spec.
  3. Verify bare spec / @latest fails with an opt-in message, while explicit prerelease selectors still install.

Expected

  • Stable-by-default specs reject prerelease resolutions.
  • Explicit prerelease selectors continue to work.

Actual

  • After the change, behavior matches the expectation above.

Evidence

  • 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: focused Vitest coverage for npm spec parsing plus plugin and hook install flows.
  • Edge cases checked: bare spec, @latest, explicit prerelease tag, exact prerelease version parsing, and semver range rejection.
  • What you did not verify: full repo build/lint/test suite.

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert the npm spec parsing/install commits on this branch.
  • Files/config to restore: src/infra/npm-registry-spec.ts and src/infra/npm-pack-install.ts.
  • Known bad symptoms reviewers should watch for: legitimate stable installs being rejected despite resolving to a non-prerelease version.

Risks and Mitigations

  • Risk: some users may have been relying on semver ranges in plugin/hook install specs even though docs said exact version/tag.
    • Mitigation: the error path is explicit, docs now match the enforced behavior, and exact versions/dist-tags remain supported.
  • Risk: prerelease detection could be too aggressive for explicit opt-in flows.
    • Mitigation: focused tests cover bare specs, @latest, and explicit prerelease selectors for both plugins and hooks.

AI-assisted: yes. Focused local verification only.

@vincentkoc vincentkoc self-assigned this Mar 6, 2026
@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation size: M maintainer Maintainer-authored PR labels Mar 6, 2026
@vincentkoc vincentkoc marked this pull request as ready for review March 6, 2026 16:13
@vincentkoc vincentkoc merged commit f392b81 into main Mar 6, 2026
@vincentkoc vincentkoc deleted the vincentkoc-code/npm-install-prerelease-opt-in branch March 6, 2026 16:13
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR successfully hardens plugin and hook npm installs by requiring explicit opt-in for prerelease resolutions. The implementation in src/infra/npm-registry-spec.ts correctly blocks prerelease resolutions for bare specs and @latest, and the check is properly integrated into the shared installFromNpmSpecArchive flow so both plugins and hooks benefit.

Key findings:

  1. UX regression in error messaging: When an npm spec is invalid (e.g. semver ranges), callers receive the generic error "unsupported npm spec" instead of specific guidance like "use an exact version or dist-tag (ranges are not allowed)". The validateRegistryNpmSpec function already produces this detail, so it should be used before parsing to provide better feedback.

  2. Policy gap with non-latest dist-tags: The implementation allows prerelease resolutions for any dist-tag except @latest (line 129). While this is correct code, it creates a mismatch with the PR's stated intent—"explicitly opts in with a prerelease tag"—which the description illustrates with @beta and @rc. Tags like @stable or @current will now silently accept prerelease resolutions if the tag points to one, even though their names imply stability. This behavior is untested and undocumented, creating a maintenance hazard. Either narrowing to a prerelease-tag allowlist or documenting the policy would reduce confusion.

The core prerelease-blocking logic is sound for the primary use cases (bare spec, @latest, exact prerelease version), and test coverage for both plugins and hooks appropriately validates the main scenarios.

Confidence Score: 3/5

  • Safe to merge with the security improvement intact; two actionable gaps remain uncorrected.
  • The PR correctly implements stable-by-default prerelease blocking for bare specs and @latest. However, two issues reduce confidence: (1) error messages regress from actionable to generic when specs are invalid, and (2) the implementation allows any non-latest dist-tag to resolve to prerelease, which creates a policy inconsistency with the stated design intent and is neither tested nor documented. Both are addressable without architectural changes.
  • src/infra/npm-pack-install.ts (error message UX) and src/infra/npm-registry-spec.ts (dist-tag prerelease policy documentation/narrowing)

Last reviewed commit: e036027

Comment on lines +102 to +108
const parsedSpec = parseRegistryNpmSpec(params.spec);
if (!parsedSpec) {
return {
ok: false,
error: "unsupported npm spec",
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When parseRegistryNpmSpec returns null, the error is hardcoded to "unsupported npm spec", which discards the specific validation reason (e.g. "use an exact version or dist-tag (ranges are not allowed)"). Since validateRegistryNpmSpec already provides that detail, callers get a far less actionable message.

Suggested change
const parsedSpec = parseRegistryNpmSpec(params.spec);
if (!parsedSpec) {
return {
ok: false,
error: "unsupported npm spec",
};
}
const specValidationError = validateRegistryNpmSpec(params.spec);
if (specValidationError) {
return {
ok: false,
error: specValidationError,
};
}
const parsedSpec = parseRegistryNpmSpec(params.spec)!;

(You'll also need to add validateRegistryNpmSpec to the import from ./npm-registry-spec.js.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/npm-pack-install.ts
Line: 102-108

Comment:
When `parseRegistryNpmSpec` returns `null`, the error is hardcoded to `"unsupported npm spec"`, which discards the specific validation reason (e.g. `"use an exact version or dist-tag (ranges are not allowed)"`). Since `validateRegistryNpmSpec` already provides that detail, callers get a far less actionable message.

```suggestion
    const specValidationError = validateRegistryNpmSpec(params.spec);
    if (specValidationError) {
      return {
        ok: false,
        error: specValidationError,
      };
    }
    const parsedSpec = parseRegistryNpmSpec(params.spec)!;
```

(You'll also need to add `validateRegistryNpmSpec` to the import from `./npm-registry-spec.js`.)

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +116 to +130
export function isPrereleaseResolutionAllowed(params: {
spec: ParsedRegistryNpmSpec;
resolvedVersion?: string;
}): boolean {
if (!params.resolvedVersion || !isPrereleaseSemverVersion(params.resolvedVersion)) {
return true;
}
if (params.spec.selectorKind === "none") {
return false;
}
if (params.spec.selectorKind === "exact-version") {
return params.spec.selectorIsPrerelease;
}
return params.spec.selector?.toLowerCase() !== "latest";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The final branch of isPrereleaseResolutionAllowed (line 129) permits prerelease resolutions for any tag whose lowercase form is not "latest". This means a user installing with @stable, @current, or @production — dist-tags that carry an implicit stable-version expectation — will silently receive a prerelease if that tag resolves to one.

The PR description mentions only @beta / @rc as explicit opt-in signals, suggesting only prerelease-named tags should allow prerelease installs. This policy gap is not tested (the test at line 50 covers only @beta) and not documented.

Consider either:

  1. Adding a known-prerelease-tag allowlist (e.g. beta, rc, alpha, canary, next, dev, experimental) and only those tags permit prerelease
  2. Checking whether a tag name contains a recognisable prerelease identifier
  3. At minimum, documenting this behavior in a code comment so maintainers understand the policy and don't accidentally widen it further
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/npm-registry-spec.ts
Line: 116-130

Comment:
The final branch of `isPrereleaseResolutionAllowed` (line 129) permits prerelease resolutions for any tag whose lowercase form is not `"latest"`. This means a user installing with `@stable`, `@current`, or `@production` — dist-tags that carry an implicit stable-version expectation — will silently receive a prerelease if that tag resolves to one.

The PR description mentions only `@beta` / `@rc` as explicit opt-in signals, suggesting only prerelease-named tags should allow prerelease installs. This policy gap is not tested (the test at line 50 covers only `@beta`) and not documented.

Consider either:
1. Adding a known-prerelease-tag allowlist (e.g. `beta`, `rc`, `alpha`, `canary`, `next`, `dev`, `experimental`) and only those tags permit prerelease
2. Checking whether a tag name contains a recognisable prerelease identifier
3. At minimum, documenting this behavior in a code comment so maintainers understand the policy and don't accidentally widen it further

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

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

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: e036027a09

ℹ️ 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".

Comment on lines +79 to +82
if (!DIST_TAG_RE.test(selector)) {
return {
ok: false,
error: "unsupported npm spec: use an exact version or dist-tag (ranges are not allowed)",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject x/major semver ranges before treating selector as tag

The new validator only rejects range selectors that fail DIST_TAG_RE, so selectors like 1.x, 1.2, and 1 are still accepted as selectorKind: "tag" even though npm treats <name>@<version range> as a range form (see npm's package-spec docs). This means the change does not actually enforce the documented “ranges are not allowed” contract for common range syntaxes, and users can still install via ranges despite the new guardrail.

Useful? React with 👍 / 👎.

ant1eicher pushed a commit to ant1eicher/openclaw that referenced this pull request Mar 6, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
Saitop pushed a commit to NomiciAI/openclaw that referenced this pull request Mar 8, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy

(cherry picked from commit f392b81)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
…38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy

(cherry picked from commit f392b81)
alexey-pelykh added a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
* fix(subagents): recover announce cleanup after kill/complete race

(cherry picked from commit 2f86ae7)

* feat(hooks): emit compaction lifecycle hooks (openclaw#16788)

(cherry picked from commit 71ec421)

* fix(agent): harden undici stream timeouts for long openai-completions runs

(cherry picked from commit 05fb16d)

* fix(agents): allow configured ollama endpoints without dummy api keys

(cherry picked from commit 36afd1b)

* feat(openai): add gpt-5.4 support for API and Codex OAuth (openclaw#36590)

Co-authored-by: Tyler Yust <[email protected]>
(cherry picked from commit 5d4b040)

* Fix failover for zhipuai 1310 Weekly/Monthly Limit Exhausted (openclaw#33813)

Co-authored-by: zhouhe-xydt <[email protected]>
Co-authored-by: altaywtf <[email protected]>
(cherry picked from commit a65d70f)

* fix(failover): classify HTTP 402 as rate_limit when payload indicates usage limit (openclaw#30484) (openclaw#36802)

Co-authored-by: Val Alexander <[email protected]>
(cherry picked from commit 01b2017)

* feat(onboarding): add web search to onboarding flow (openclaw#34009)

* add web search to onboarding flow

* remove post onboarding step (now redundant)

* post-onboarding nudge if no web search set up

* address comments

* fix test mocking

* add enabled: false assertion to the no-key test

* --skip-search cli flag

* use provider that a user has a key for

* add assertions, replace the duplicated switch blocks

* test for quickstart fast-path with existing config key

* address comments

* cover quickstart falls through to key test

* bring back key source

* normalize secret inputs instead of direct string trimming

* preserve enabled: false if it's already set

* handle missing API keys in flow

* doc updates

* hasExistingKey to detect both plaintext strings and SecretRef objects

* preserve enabled state only on the "keep current" paths

* add test for preserving

* better gate flows

* guard against invalid provider values in config

* Update src/commands/configure.wizard.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* format fix

* only mentions env var when it's actually available

* search apiKey fields now typed as SecretInput

* if no provider check if any search provider key is detectable

* handle both kimi keys

* remove .filter(Boolean)

* do not disable web_search after user enables it

* update resolveSearchProvider

* fix(onboarding): skip search key prompt in ref mode

* fix: add onboarding web search step (openclaw#34009) (thanks @kesku)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Shadow <[email protected]>
(cherry picked from commit 3d7bc59)

* fix(ci): restore protocol and schema checks (openclaw#37470)

(cherry picked from commit ee6f7b1)

* Infra: require explicit opt-in for prerelease npm installs (openclaw#38117)

* Infra: tighten npm registry spec parsing

* Infra: block implicit prerelease npm installs

* Plugins: cover prerelease install policy

* Infra: add npm registry spec tests

* Hooks: cover prerelease install policy

* Docs: clarify plugin guide version policy

* Docs: clarify plugin install version policy

* Docs: clarify hooks install version policy

* Docs: clarify hook pack version policy

(cherry picked from commit f392b81)

* ci: trigger workflow

* fix: resolve cherry-pick adaptation failures from upstream web search commit

- Add missing hasConfiguredSecretInput() to types.secrets.ts
- Add SecretInput import to types.tools.ts for apiKey fields
- Replace OpenClawConfig with RemoteClawConfig in onboard-search
- Remove non-existent DEFAULT_SECRET_PROVIDER_ALIAS import
- Remove invalid 'provider' field from SecretRef literal
- Stub resolveSyntheticLocalProviderAuth (models.providers was gutted)
- Replace undefined registerRun/emitLifecycleEnd test helpers with
  existing mod.registerSubagentRun/lifecycleHandler patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Plugins: clarify registerHttpHandler migration errors (openclaw#36794)

* Changelog: note plugin HTTP route migration diagnostics

* Tests: cover registerHttpHandler migration diagnostics

* Plugins: clarify registerHttpHandler migration errors

* Tests: cover registerHttpHandler diagnostic edge cases

* Plugins: tighten registerHttpHandler migration hint

(cherry picked from commit d4021f4)

* Plugins: avoid false integrity drift prompts on unpinned updates (openclaw#37179)

* Plugins: skip drift prompts for unpinned updates

* Plugins: cover unpinned integrity update behavior

(cherry picked from commit 428d176)

* docs(protocol): document slash-delimited schema lookup plugin ids

(cherry picked from commit f788ba1)

* docs(plugins): document context engine slots and registration

(cherry picked from commit eb2eeba)

* docs(plugins): add context-engine manifest kind example

(cherry picked from commit 7cc3376)

* docs(config): list the context engine plugin slot

(cherry picked from commit 5470337)

* fix: Windows: openclaw plugins install fails with spawn EINVAL

Fixes openclaw#7631

(cherry picked from commit d000316)

* fix(whatsapp): remove implicit [openclaw] self-chat prefix

(cherry picked from commit 4d9134f)

* WhatsApp: honor outbound mediaMaxMb (openclaw#38097)

* WhatsApp: add media cap helper

* WhatsApp: cap outbound media loads

* WhatsApp: align auto-reply media caps

* WhatsApp: add outbound media cap test

* WhatsApp: update auto-reply cap tests

* Docs: update WhatsApp media caps

* Changelog: note WhatsApp media cap fix

(cherry picked from commit 222d635)

* fix(mattermost): allow reachable interaction callback URLs (openclaw#37543)

Merged via squash.

Prepared head SHA: 4d59373
Co-authored-by: mukhtharcm <[email protected]>
Co-authored-by: mukhtharcm <[email protected]>
Reviewed-by: @mukhtharcm

(cherry picked from commit 4a80d48)

* Mattermost: harden interaction callback binding (openclaw#38057)

(cherry picked from commit a274ef9)

* chore: code/dead tests cleanup (openclaw#38286)

* Discord: assert bot-self filter queue guard

* Tests: remove dead gateway SIGTERM placeholder

(cherry picked from commit 38f46e8)

* Delete changelog/fragments directory

(cherry picked from commit 8f69e07)

* fix(feishu): restore explicit media request timeouts

(cherry picked from commit b127333)

* fix: adapt upstream naming to fork conventions

Replace OpenClawConfig with RemoteClawConfig, openclaw/plugin-sdk with
remoteclaw/plugin-sdk, and fix import paths for fork type exports.

* fix: resolve type errors from upstream cherry-picks

- Add missing RemoteClawConfig import in discord test
- Use spread with as-any for feishu SDK timeout property
- Remove allowUnresolvedSecretRef from mattermost test

* ci: re-trigger CI after force push

* fix: revert unintended onboarding changes from linter bleed

* ci: sync PR head with branch

---------

Co-authored-by: Vignesh Natarajan <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: dorukardahan <[email protected]>
Co-authored-by: Tyler Yust <[email protected]>
Co-authored-by: zhouhe-xydt <[email protected]>
Co-authored-by: zhouhe-xydt <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: Xinhua Gu <[email protected]>
Co-authored-by: Val Alexander <[email protected]>
Co-authored-by: Kesku <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Shadow <[email protected]>
Co-authored-by: Altay <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: 0xlin2023 <[email protected]>
Co-authored-by: Muhammed Mukhthar CM <[email protected]>
Co-authored-by: Ayaan Zaidi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant