Skip to content

feat: add bundled Chutes extension#49136

Merged
steipete merged 2 commits intomainfrom
steipete/land-pr41416-chutes
Mar 17, 2026
Merged

feat: add bundled Chutes extension#49136
steipete merged 2 commits intomainfrom
steipete/land-pr41416-chutes

Conversation

@steipete
Copy link
Copy Markdown
Contributor

Summary

  • add bundled Chutes provider extension with plugin-owned OAuth and API-key auth
  • move bundled-provider default enablement + auth discovery onto generic extension seams
  • add coverage for default-on bundled loading, provider auth discovery, and Chutes implicit discovery

Notes

Verification

  • pnpm build
  • pnpm check
  • pnpm tsgo
  • OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test -- src/plugins/providers.test.ts src/plugins/provider-catalog.test.ts src/plugins/config-state.test.ts src/plugins/manifest-registry.test.ts src/plugins/provider-discovery.test.ts src/plugins/contracts/discovery.contract.test.ts src/plugins/contracts/registry.contract.test.ts src/plugins/contracts/loader.contract.test.ts src/plugins/loader.test.ts src/agents/model-auth-markers.test.ts src/commands/auth-choice.test.ts src/agents/chutes-models.test.ts
  • live Chutes discovery with real CHUTES_API_KEY from shell: implicit bundled provider resolved, 47 models returned
  • manual regression script covering the Chutes implicit-discovery assertions from src/agents/models-config.providers.chutes.test.ts

@steipete steipete requested a review from a team as a code owner March 17, 2026 16:35
@cursor
Copy link
Copy Markdown

cursor Bot commented Mar 17, 2026

PR Summary

Medium Risk
Adds a new default-on provider plugin and changes how bundled providers are enabled and how OAuth vs API-key credentials are surfaced to provider catalog discovery, which could affect provider loading and model discovery behavior across plugins.

Overview
Adds a new bundled, enabled-by-default chutes provider extension with plugin-owned OAuth and API-key auth flows, plus dynamic model discovery that falls back to a static catalog and applies Chutes-specific onboarding config/aliases.

Generalizes provider discovery/auth plumbing by introducing resolveProviderAuth (mode/source + optional OAuth marker) and a new oauth:<provider> marker, and wiring this through runProviderCatalog/implicit provider resolution so catalogs can use OAuth access tokens for discovery without treating them as persisted API keys.

Updates bundled plugin loading to honor enabledByDefault from openclaw.plugin.json (and adds compat shims to auto-enable bundled provider plugins during allowlist/vitest compat), and refactors provider contract registry building to derive bundled provider entries from the plugin loader instead of a hardcoded list. Removes the legacy Chutes OAuth handling from auth-choice.apply.oauth.ts and adds/adjusts tests accordingly.

Written by Cursor Bugbot for commit 7255cb9. This will update automatically on new commits. Configure here.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Mar 17, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟡 Medium Overbroad oauth: API key marker classification can bypass masking/auditing and leak secrets
2 🔵 Low Cleartext storage of Chutes access tokens in in-memory cache key
3 🔵 Low OAuth state validation bypass via code-only paste in Chutes manual login flow

1. 🟡 Overbroad oauth: API key marker classification can bypass masking/auditing and leak secrets

Property Value
Severity Medium
CWE CWE-532
Location src/agents/model-auth-markers.ts:45-86

Description

The new OAuth API key marker scheme treats any value beginning with "oauth:" as a non-secret marker. This can misclassify real secrets/tokens that happen to start with that prefix, leading to disclosure and audit bypass.

Impact:

  • Masking bypass / disclosure in CLI output: commands/models/list.auth-overview.ts prints non-secret markers verbatim as marker(<value>). If a real token is set to e.g. apiKey: "oauth:REALSECRET...", the command will output the full token instead of masking it.
  • Secret audit bypass: secrets/audit.ts uses isNonSecretApiKeyMarker() to decide whether an apiKey is plaintext. Any oauth:* value will be treated as non-secret and will not be flagged as plaintext even if it is a real credential.

Vulnerable code (marker classification):

export function isOAuthApiKeyMarker(value: string): boolean {
  return value.trim().startsWith(OAUTH_API_KEY_MARKER_PREFIX);
}
...
trimmed === QWEN_OAUTH_MARKER ||
isOAuthApiKeyMarker(trimmed) ||
...

Vulnerable sink example (verbatim output):

return isNonSecretApiKeyMarker(value, { includeEnvVarName: false })
  ? `marker(${value.trim()})`
  : maskApiKey(value);

Concrete scenario:

  1. User (or a malicious config/template) sets models.json provider apiKey to "oauth:..." where ... is an actual secret.
  2. Running models list --auth-overview will print marker(oauth:...) revealing the secret.
  3. Running the secrets audit will not warn about plaintext storage because the value is treated as a marker.

This is a regression compared to enumerating only specific marker constants (e.g., qwen-oauth) and makes the marker namespace trivially collidable with real secrets.

Recommendation

Narrow marker recognition so arbitrary oauth:* strings are not automatically treated as non-secret.

Recommended changes:

  • Validate providerId and only recognize markers that match the exact canonical form for known providers (or a strict allowed-id regex).
  • Consider parsing as oauth:<providerId> and ensuring <providerId> is in the provider catalog (or allowlist).
  • At minimum, do not print marker values verbatim; print a generic label (e.g. marker(oauth) or marker(oauth:<provider>)) and/or mask the remainder.

Example hardening:

const OAUTH_MARKER_RE = /^oauth:([a-z0-9][a-z0-9-]{0,62})$/i;

export function isOAuthApiKeyMarker(value: string, knownProviders?: Set<string>): boolean {
  const m = value.trim().match(OAUTH_MARKER_RE);
  if (!m) return false;
  const providerId = m[1].toLowerCase();
  return knownProviders ? knownProviders.has(providerId) : true;
}

export function resolveOAuthApiKeyMarker(providerId: string): string {
  const normalized = providerId.trim().toLowerCase();
  if (!OAUTH_MARKER_RE.test(`oauth:${normalized}`)) {
    throw new Error("Invalid providerId for oauth marker");
  }
  return `oauth:${normalized}`;
}

Also update sinks like formatMarkerOrSecret() to avoid echoing untrusted marker strings verbatim.


2. 🔵 Cleartext storage of Chutes access tokens in in-memory cache key

Property Value
Severity Low
CWE CWE-316
Location src/agents/chutes-models.ts:492-496

Description

discoverChutesModels(accessToken?) stores the raw (trimmed) access token string as the key in a process-wide Map cache.

  • Input: accessToken (API key / OAuth access token)
  • Issue: token is retained in memory as a map key for up to 5 minutes (TTL) and potentially longer if not pruned immediately, increasing exposure risk via heap dumps, crash reports, debugging/profiling tools, or accidental serialization/inspection.
  • Impact: unintended disclosure of bearer tokens can allow account/API impersonation.

Vulnerable code:

// Keyed by trimmed access token (empty string = unauthenticated).
const modelCache = new Map<string, CacheEntry>();

Recommendation

Avoid keeping raw bearer tokens in long-lived data structures. Instead, derive a non-reversible cache key.

Example (SHA-256 hash as cache key):

import { createHash } from "node:crypto";

function tokenToCacheKey(token: string | undefined): string {
  const trimmed = token?.trim() ?? "";
  if (!trimmed) return "";
  return createHash("sha256").update(trimmed, "utf8").digest("hex");
}

export async function discoverChutesModels(accessToken?: string) {
  const cacheKey = tokenToCacheKey(accessToken);
  const rawToken = accessToken?.trim() ?? "";

  const cached = modelCache.get(cacheKey);
  if (cached) return cached.models;

  const headers: Record<string, string> = {};
  if (rawToken) headers.Authorization = `Bearer ${rawToken}`;// ...
}

This preserves token scoping without retaining the token itself in the cache key. Also consider minimizing TTL and ensuring cache entries are promptly cleared on shutdown or auth revocation.


3. 🔵 OAuth state validation bypass via code-only paste in Chutes manual login flow

Property Value
Severity Low
CWE CWE-352
Location src/commands/chutes-oauth.ts:18-44

Description

The Chutes OAuth "manual" (copy/paste) flow accepts a raw authorization code without requiring or validating the OAuth state value.

  • loginChutes() uses parseManualOAuthInput() when manual: true (remote/VPS flow)
  • If the pasted input does not "look like" a URL/querystring, it is treated as a raw authorization code and the function silently injects the expected state rather than validating a returned state
  • This weakens CSRF protection / login integrity for the manual flow and can enable account substitution if an attacker can obtain a valid authorization code for the in-progress PKCE challenge and trick the user into pasting it

Vulnerable code:

// Support pasting either:// - Full redirect URL (preferred; validates state)// - Raw authorization code (legacy/manual copy flows)
const looksLikeRedirect =/^https?:\/\//i.test(trimmed) || trimmed.includes("://") || trimmed.includes("?");
if (!looksLikeRedirect) {
  return { code: trimmed, state: expectedState };
}

Even though parseOAuthCallbackInput() correctly rejects code-only input, this wrapper reintroduces code-only acceptance and bypasses state validation.

Recommendation

Require state verification for all manual paste inputs.

  • Remove support for code-only pastes (or gate it behind an explicit --allow-insecure-code-paste debug flag)
  • Parse querystring-only input robustly (including inputs like code=...&state=... without a leading ?)

Suggested fix:

function parseManualOAuthInput(input: string, expectedState: string) {
  const trimmed = String(input ?? "").trim();
  if (!trimmed) throw new Error("Missing OAuth redirect URL.");// Always require state-bearing redirect URL or querystring.
  const parsed = parseOAuthCallbackInput(trimmed, expectedState);
  if ("error" in parsed) throw new Error(parsed.error);
  return parsed;
}

This ensures the manual flow cannot proceed unless the pasted content includes a matching state.


Analyzed PR: #49136 at commit 7255cb9

Last updated on: 2026-03-17T17:10:29Z

@steipete steipete merged commit a724bbc into main Mar 17, 2026
12 checks passed
@steipete steipete deleted the steipete/land-pr41416-chutes branch March 17, 2026 16:35
@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Mar 17, 2026
@steipete
Copy link
Copy Markdown
Contributor Author

Landed via squash onto main.

  • Gate: pnpm build; pnpm check; pnpm tsgo; OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test -- src/plugins/providers.test.ts src/plugins/provider-catalog.test.ts src/plugins/config-state.test.ts src/plugins/manifest-registry.test.ts src/plugins/provider-discovery.test.ts src/plugins/contracts/discovery.contract.test.ts src/plugins/contracts/registry.contract.test.ts src/plugins/contracts/loader.contract.test.ts src/plugins/loader.test.ts src/agents/model-auth-markers.test.ts src/commands/auth-choice.test.ts src/agents/chutes-models.test.ts; live Chutes implicit-discovery check with real CHUTES_API_KEY; manual regression script for src/agents/models-config.providers.chutes.test.ts
  • Refactor commit: 597d38f
  • Chutes commit: 7255cb9
  • Merge commit: a724bbc

Thanks @Veightor!

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

id.toLowerCase().includes("r1") ||
id.toLowerCase().includes("thinking") ||
id.toLowerCase().includes("reason") ||
id.toLowerCase().includes("tee");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overly broad substring match misclassifies reasoning models

Low Severity

The dynamic discovery reasoning heuristic uses id.toLowerCase().includes("r1") and id.toLowerCase().includes("tee") which are overly broad substring checks. "r1" matches any model ID containing that substring anywhere (e.g., "provider1/some-model", "gpt4-mar1-release"), and "tee" matches words like "committee" or "guarantee". This can incorrectly flag non-reasoning models as reasoning-capable during live API discovery, affecting model selection and routing.

Fix in Cursor Fix in Web

},
},
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exported applyChutesConfig function is never imported

Low Severity

applyChutesConfig is exported from extensions/chutes/onboard.ts but is never imported or referenced anywhere in the codebase. The function applyChutesProviderConfig and applyChutesApiKeyConfig are both used, but applyChutesConfig (which sets the default model and image model to Chutes) is dead code.

Fix in Cursor Fix in Web

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 17, 2026

Greptile Summary

This PR adds a bundled Chutes.ai provider extension with plugin-owned OAuth and API-key authentication, while simultaneously generalizing the enablement and provider-auth discovery seams so future bundled plugins can declare enabledByDefault: true in their manifest and expose rich auth context (env, api-key profile, or OAuth profile) to their catalog hooks.

Key changes:

  • New extensions/chutes/ plugin with OAuth flow, API-key auth, dynamic model discovery with a token-scoped, TTL-bounded cache, and a 47-model static fallback catalog.
  • Chutes-specific OAuth code removed from the core auth-choice.apply.oauth.ts (now a no-op stub).
  • New resolveProviderAuth resolver added to ImplicitProviderContext / ProviderCatalogContext, exposing auth mode (env, api_key, oauth) and a discoveryApiKey separate from the API-key marker — enabling authenticated model discovery for OAuth users.
  • enabledByDefault field threaded from PluginManifestPluginManifestRecordresolveEnableState, allowing any bundled plugin to opt into default-on behaviour without touching BUNDLED_ENABLED_BY_DEFAULT.
  • src/plugins/contracts/registry.ts refactored to derive its provider registry from resolvePluginProviders rather than a hard-coded import list.
  • Comprehensive test coverage across model catalog, caching, implicit discovery, auth precedence, and plugin loading.

Minor issues flagged:

  • CHUTES_DEFAULT_COST is exported from chutes-models.ts but is not used internally, not re-exported through the plugin SDK, and not referenced in the catalog or discovery paths — it is dead code.
  • The id.toLowerCase().includes("tee") heuristic for reasoning classification is broader than the TEE suffix pattern it intends to capture, and could silently misclassify future models whose IDs contain the substring for unrelated reasons.

Confidence Score: 5/5

  • Safe to merge — no runtime errors, security issues, or breaking logic changes found; the two flagged items are style-level concerns.
  • The PR is well-structured with clear separation of concerns, comprehensive test coverage for all new paths (caching, auth precedence, discovery, plugin loading), and a clean removal of the old hardcoded Chutes OAuth block. The two flagged issues (CHUTES_DEFAULT_COST dead export and the broad tee heuristic) are minor quality concerns that do not affect correctness in the current Chutes model namespace.
  • No files require special attention for correctness; src/agents/chutes-models.ts has the two style-level findings noted above.

Comments Outside Diff (2)

  1. src/agents/chutes-models.ts, line 1298-1303 (link)

    P2 tee substring may produce false-positive reasoning classifications

    The heuristic marks any model whose ID contains the lowercase substring "tee" as a reasoning model:

    id.toLowerCase().includes("tee")

    While Chutes uses TEE (Trusted Execution Environment) exclusively for reasoning-capable models today, this substring can inadvertently match unrelated model IDs (e.g., "committee", "attestation", any vendor name containing the three letters in sequence). If Chutes ever lists a non-reasoning model whose ID happens to contain "tee", it will be silently misclassified, potentially leading to unexpected prompting behavior.

    Consider tightening the check to match only a word-boundary or suffix pattern, e.g.:

    /-tee$/i.test(id) || id.toUpperCase().endsWith("-TEE")

    or simply rely solely on entry.supported_features?.includes("reasoning") and the explicit r1 / thinking / reason checks, treating TEE as a naming annotation that the API should report through supported_features.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/chutes-models.ts
    Line: 1298-1303
    
    Comment:
    **`tee` substring may produce false-positive reasoning classifications**
    
    The heuristic marks any model whose ID contains the lowercase substring `"tee"` as a reasoning model:
    
    ```ts
    id.toLowerCase().includes("tee")
    ```
    
    While Chutes uses `TEE` (Trusted Execution Environment) exclusively for reasoning-capable models today, this substring can inadvertently match unrelated model IDs (e.g., `"committee"`, `"attestation"`, any vendor name containing the three letters in sequence). If Chutes ever lists a non-reasoning model whose ID happens to contain `"tee"`, it will be silently misclassified, potentially leading to unexpected prompting behavior.
    
    Consider tightening the check to match only a word-boundary or suffix pattern, e.g.:
    
    ```ts
    /-tee$/i.test(id) || id.toUpperCase().endsWith("-TEE")
    ```
    
    or simply rely solely on `entry.supported_features?.includes("reasoning")` and the explicit `r1` / `thinking` / `reason` checks, treating TEE as a naming annotation that the API should report through `supported_features`.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/agents/chutes-models.ts, line 710-715 (link)

    P2 CHUTES_DEFAULT_COST is exported but never used

    CHUTES_DEFAULT_COST is exported from this file but is neither consumed within chutes-models.ts itself nor re-exported through src/plugin-sdk/provider-models.ts. The static model catalog entries each define their own explicit costs, and the dynamic discovery path builds cost objects inline (entry.pricing?.prompt || 0, etc.). As a result, this constant is dead code.

    If it is intended as a convenience export for plugin authors, it should be added to the provider-models.ts SDK re-exports. Otherwise it can be removed to avoid confusion.

    (Remove lines 710–715 if unused, or add CHUTES_DEFAULT_COST to the src/plugin-sdk/provider-models.ts export list if it is meant to be part of the public SDK surface.)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/chutes-models.ts
    Line: 710-715
    
    Comment:
    **`CHUTES_DEFAULT_COST` is exported but never used**
    
    `CHUTES_DEFAULT_COST` is exported from this file but is neither consumed within `chutes-models.ts` itself nor re-exported through `src/plugin-sdk/provider-models.ts`. The static model catalog entries each define their own explicit costs, and the dynamic discovery path builds cost objects inline (`entry.pricing?.prompt || 0`, etc.). As a result, this constant is dead code.
    
    If it is intended as a convenience export for plugin authors, it should be added to the `provider-models.ts` SDK re-exports. Otherwise it can be removed to avoid confusion.
    
    
    (Remove lines 710–715 if unused, or add `CHUTES_DEFAULT_COST` to the `src/plugin-sdk/provider-models.ts` export list if it is meant to be part of the public SDK surface.)
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/chutes-models.ts
Line: 1298-1303

Comment:
**`tee` substring may produce false-positive reasoning classifications**

The heuristic marks any model whose ID contains the lowercase substring `"tee"` as a reasoning model:

```ts
id.toLowerCase().includes("tee")
```

While Chutes uses `TEE` (Trusted Execution Environment) exclusively for reasoning-capable models today, this substring can inadvertently match unrelated model IDs (e.g., `"committee"`, `"attestation"`, any vendor name containing the three letters in sequence). If Chutes ever lists a non-reasoning model whose ID happens to contain `"tee"`, it will be silently misclassified, potentially leading to unexpected prompting behavior.

Consider tightening the check to match only a word-boundary or suffix pattern, e.g.:

```ts
/-tee$/i.test(id) || id.toUpperCase().endsWith("-TEE")
```

or simply rely solely on `entry.supported_features?.includes("reasoning")` and the explicit `r1` / `thinking` / `reason` checks, treating TEE as a naming annotation that the API should report through `supported_features`.

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

---

This is a comment left during a code review.
Path: src/agents/chutes-models.ts
Line: 710-715

Comment:
**`CHUTES_DEFAULT_COST` is exported but never used**

`CHUTES_DEFAULT_COST` is exported from this file but is neither consumed within `chutes-models.ts` itself nor re-exported through `src/plugin-sdk/provider-models.ts`. The static model catalog entries each define their own explicit costs, and the dynamic discovery path builds cost objects inline (`entry.pricing?.prompt || 0`, etc.). As a result, this constant is dead code.

If it is intended as a convenience export for plugin authors, it should be added to the `provider-models.ts` SDK re-exports. Otherwise it can be removed to avoid confusion.

```suggestion
```
(Remove lines 710–715 if unused, or add `CHUTES_DEFAULT_COST` to the `src/plugin-sdk/provider-models.ts` export list if it is meant to be part of the public SDK surface.)

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

Last reviewed commit: 7255cb9

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: 7255cb9eb5

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

return { config: nextConfig };
}

return null;
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 Handle unresolved Chutes OAuth choice explicitly

This function now always returns null, so applyAuthChoice has no fallback behavior when authChoice is "chutes" but the Chutes plugin is not loaded (for example, it is denylisted/disabled or failed to load). Because chutes is still exposed as a core auth choice, that selection can silently no-op and leave auth/config unchanged instead of completing OAuth or surfacing an actionable error.

Useful? React with 👍 / 👎.

Comment thread src/plugins/providers.ts
Comment on lines +179 to +183
params.bundledProviderAllowlistCompat || params.bundledProviderVitestCompat
? withBundledPluginEnablementCompat({
config: maybeVitestCompat,
pluginIds: bundledProviderCompatPluginIds,
})
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 Avoid force-enabling all bundled provider plugins in compat mode

This branch now injects plugins.entries.<id>.enabled = true for every bundled provider whenever either compat flag is set, which changes semantics from allowlist/Vitest compatibility into unconditional enablement. In practice, paths that call resolvePluginProviders(..., { bundledProviderAllowlistCompat: true }) will activate bundled providers that were intentionally default-off (for example provider plugins not in BUNDLED_ENABLED_BY_DEFAULT), expanding runtime provider surface unexpectedly.

Useful? React with 👍 / 👎.

nikolaisid pushed a commit to nikolaisid/openclaw that referenced this pull request Mar 18, 2026
* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)
ralyodio pushed a commit to ralyodio/openclaw that referenced this pull request Apr 3, 2026
* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)
juancatorr added a commit to juancatorr/openclaw that referenced this pull request Apr 8, 2026
* refactor(slack): share setup wizard base

* refactor(discord): share setup wizard base

* refactor(signal): reuse shared setup security

* refactor(imessage): reuse shared setup security

* refactor(setup): reuse patched adapters across channels

* refactor(imessage): share setup status base

* refactor(slack): share token credential setup

* refactor(setup): share env-aware patched adapters

* refactor(discord): use shared plugin base

* refactor(outbound): share base session helpers

* refactor(whatsapp): reuse login tool implementation

* refactor(providers): reuse simple api-key catalog helper

* refactor(plugins): share bundle path list helpers

* refactor(slack): reuse shared action adapter

* refactor(tts): share provider readiness checks

* refactor(plugins): share claiming hook loop

* refactor(plugins): share install target flow

* refactor(status): share scan helper state

* refactor(usage): share legacy pi auth token lookup

* refactor(device): share missing-scope helper

* refactor(config): share schema lookup helpers

* fix(plugin-sdk): restore core export boundary

* build: tighten lazy runtime boundaries

* fix(gateway): surface env override keys in exec approvals

* Agents: move bootstrap warnings out of system prompt (openclaw#48753)

Merged via squash.

Prepared head SHA: dc1d4d0
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob

* refactor: dedupe channel entrypoints and test bridges

* style: fix rebase formatting drift

* fix: resolve rebase type fallout in channel setup seams

* fix(macos): block canvas symlink escapes

* refactor: bundle lazy runtime surfaces

* fix: remove discord setup rebase marker

* docs(gateway): clarify URL allowlist semantics

* docs(changelog): restore 2026.2.27 heading

* fix: unblock full gate

* fix: stabilize full gate

* test: cover invalid main job store load

* build(test): ignore vitest scratch root

* refactor: dedupe bundled plugin entrypoints

* docs: add context engine documentation

Add dedicated docs page for the pluggable context engine system:
- Full lifecycle explanation (ingest, assemble, compact, afterTurn)
- Legacy engine behavior documentation
- Plugin engine authoring guide with code examples
- ContextEngine interface reference table
- ownsCompaction semantics
- Subagent lifecycle hooks (prepareSubagentSpawn, onSubagentEnded)
- systemPromptAddition mechanism
- Relationship to compaction, memory plugins, and session pruning
- Configuration reference and tips

Also:
- Add context-engine to docs nav (Agents > Fundamentals, after Context)
- Add /context-engine redirect
- Cross-link from context.md and compaction.md

* docs: add plugin installation steps to context engine page

Show the full workflow: install via openclaw plugins install,
enable in plugins.entries, then select in plugins.slots.contextEngine.
Uses lossless-claw as the concrete example.

* docs: address review feedback on context-engine page

- Rename 'Method' column to 'Member' with explicit Kind column since
  info is a property, not a callable method
- Document AssembleResult fields (estimatedTokens, systemPromptAddition)
  with types and optionality
- Add lifecycle timing notes for bootstrap, ingestBatch, and dispose
  so plugin authors know when each is invoked

* docs: fix context engine review notes

* fix: harden telegram and loader contracts

* refactor(tests): share setup wizard prompter

* refactor(telegram-tests): share native command helpers

* fix(telegram-tests): load plugin mocks before commands

* refactor(telegram-tests): share webhook settlement helper

* refactor(nextcloud-tests): share inbound authz setup

* refactor(feishu-tests): share card action event builders

* refactor(runtime-tests): share typing lease assertions

* refactor(hook-tests): share subagent hook helpers

* refactor(provider-tests): share discovery catalog helpers

* refactor(command-tests): share workspace harness

* refactor(contracts): share session binding assertions

* refactor(plugin-tests): share interactive dispatch assertions

* refactor(plugin-tests): share binding approval resolution

* refactor(usage-tests): share provider usage loader harness

* refactor(bundle-tests): share bundle mcp fixtures

* refactor(provider-tests): share codex catalog assertions

* refactor(apns-tests): share relay push params

* refactor(media-tests): share telegram redaction assertion

* refactor(heartbeat-tests): share seeded heartbeat run

* refactor(kilocode-tests): share reasoning payload capture

* refactor(kilocode-tests): share extra-params harness

* refactor(kilocode-tests): share cache retention wrapper

* refactor(attempt-tests): share wrapped stream helper

* refactor(payload-tests): share empty payload assertion

* Telegram: fix named-account DM topic session keys (openclaw#48773)

* refactor(compaction-tests): share aggregate timeout params

* refactor(compaction-tests): share snapshot assertions

* refactor(truncation-tests): share first tool result text helper

* refactor(system-prompt-tests): share session setup helper

* refactor(lanes-tests): share table-driven assertions

* refactor(google-tests): share schema tool fixture

* refactor(extension-tests): share safeguard factory setup

* refactor(openrouter-tests): share state dir helper

* refactor(thinking-tests): share assistant drop helper

* refactor(kilocode-tests): share eligibility assertions

* refactor(payload-tests): share empty payload helper

* refactor(model-tests): share template model mock helper

* refactor(image-tests): share empty prompt image assertions

* fix: restore full gate

* refactor: consolidate lazy runtime surfaces

* refactor: remove remaining extension core imports

* refactor(payload-tests): reuse empty payload helper

* refactor(image-tests): share empty ref assertions

* refactor(image-tests): share single-ref detection helper

* refactor(image-tests): share ref count assertions

* refactor(history-tests): share array content assertion

* refactor(runs-tests): share run handle factory

* refactor(skills-tests): share bundled diffs setup

* refactor(payload-tests): share single payload summary assertion

* refactor(payload-tests): table-drive recoverable tool suppressions

* refactor(payload-tests): table-drive sessions send suppressions

* refactor(history-tests): share pruned image assertions

* refactor(failover-tests): share observation base

* refactor(extension-tests): share safeguard runtime assertions

* fix(ci): quote changed extension matrix input

* refactor: split plugin testing seam from bundled extension helpers

* test: fix discord provider helper import

* Changelog: add Telegram DM topic session-key fix

* fix(ci): harden zizmor workflow diffing

* feat(image-generation): add image_generate tool

* test(image-generation): add live variant coverage

* docs(image-generation): remove nano banana stock docs

* fix(ci): restore local check suite

* chore: sync pnpm lockfile importers

* fix(ui): restore control-ui query token compatibility (openclaw#43979)

* fix(ui): restore control-ui query token imports

* chore(changelog): add entry for openclaw#43979 thanks @stim64045-spec

---------

Co-authored-by: 大禹 <dayu@dayudeMac-mini.local>
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>

* fix: update macOS node service to use current CLI command shape (closes openclaw#43171) (openclaw#46843)

Merged via squash.

Prepared head SHA: dbf2edd
Co-authored-by: Br1an67 <29810238+Br1an67@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF

* fix(macos): stop relaunching the app after quit when launch-at-login is enabled (openclaw#40213)

Merged via squash.

Prepared head SHA: c702d98
Co-authored-by: stablegenius49 <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF

* tests: add missing useNoBundledPlugins() to bundle MCP loader test

The "treats bundle MCP as a supported bundle surface" test was missing
the useNoBundledPlugins() call present in all surrounding bundle plugin
tests. Without it, loadOpenClawPlugins() scanned and loaded the full
real bundled plugins directory on every call (with cache:false), causing
excessive memory pressure and an OOM crash on Linux CI, which manifested
as the test timing out at 120s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix ssh sandbox key cp (openclaw#48924)

Signed-off-by: sallyom <somalley@redhat.com>

* tests: fix googlechat outbound partial mock

* tests(google): inject oauth credential fs stubs

* tests(feishu): mock conversation runtime seam

* tests(feishu): inject client runtime seam

* tests(contracts): fix provider catalog runtime wiring (openclaw#49040)

* fix(plugins): forward plugin subagent overrides (openclaw#48277)

Merged via squash.

Prepared head SHA: ffa4589
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* fix(security): block JVM, Python, and .NET env injection vectors in host exec sandbox (openclaw#49025)

Add JAVA_TOOL_OPTIONS, _JAVA_OPTIONS, JDK_JAVA_OPTIONS, PYTHONBREAKPOINT, and
DOTNET_STARTUP_HOOKS to blockedKeys in the host exec security policy.

Closes openclaw#22681

* CI: rename startup memory smoke (openclaw#49041)

* CI: guard gateway watch against duplicate runtime regressions (openclaw#49048)

* fix(hooks): pass sessionFile and sessionKey in after_compaction hook (openclaw#40781)

Merged via squash.

Prepared head SHA: 11e85f8
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* refactor: expose lazy runtime helper to plugins

* refactor: align telegram test support with plugin runtime seam

* fix(tlon): defer DM cite expansion until after auth

* fix(context-engine): preserve legacy plugin sessionKey interop (openclaw#44779)

Merged via squash.

Prepared head SHA: e04c6fb
Co-authored-by: hhhhao28 <112874572+hhhhao28@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* test: harden CI-sensitive test suites

* test: remove repeated update module imports

* test: inline bluebubbles action mocks

* test: reuse subagent orphan recovery imports

* test: flatten twitch send mocks

* test: stabilize pdf tool runtime mocks

* test: reuse run-node module imports

* test: reuse git commit module exports

* test: trim redundant context engine assertions

* test: merge duplicate update cli scenarios

* test: preload plugin sdk subpath imports

* test: cache provider discovery fixtures

* test: merge context lookup warmup cases

* test: trim lightweight status and capability suites

* test: merge telegram action matrix cases

* test: merge embeddings provider selection cases

* test: preload inbound contract fixtures

* test: merge message action media sandbox cases

* test: merge pid alive linux stat cases

* test: merge tts config gating cases

* test: trim signal and slack action cases

* test: harden commands test module seams

* test: merge bundle loader fixture cases

* test: merge loader cache partition cases

* test: merge loader setup entry matrix

* test: merge discord audit allowlist cases

* test: merge zalouser audit group cases

* test: merge audit auth precedence cases

* test: merge channel command audit cases

* test: merge browser control audit cases

* test: merge control ui audit cases

* test: merge feishu audit doc cases

* test: merge hooks audit risk cases

* test: merge gateway http audit cases

* test: share audit exposure severity helper

* test: merge loader provenance path cases

* test: merge loader escape path cases

* test: merge loader alias resolution cases

* test: merge install metadata audit cases

* test: merge loader duplicate registration cases

* test: merge loader http route cases

* test: merge audit extension and workspace cases

* test: merge loader single-plugin registration cases

* test: merge loader precedence cases

* test: merge audit exposure heuristic cases

* test: merge loader workspace warning cases

* test: merge audit hooks ingress cases

* test: merge audit extension allowlist severity cases

* test: merge loader provenance warning cases

* test: merge loader bundled telegram cases

* test: merge loader memory slot cases

* test: merge loader scoped load cases

* test: merge audit gateway auth presence cases

* test: merge audit browser container cases

* test: merge audit windows acl cases

* test: merge audit deny command cases

* test: merge audit code safety failure cases

* test: merge loader cache miss cases

* test: merge update cli service refresh cases

* test: merge slack action mapping cases

* test: merge command owner gating cases

* test: merge update status output cases

* test: merge command gateway config permission cases

* test: merge command approval scope cases

* test: merge command hook cases

* test: merge command config write denial cases

* test: merge command allowlist add cases

* test: merge update cli service refresh behavior

* test: merge command owner show gating cases

* test: merge discord action listing cases

* test: merge signal reaction mapping cases

* test: merge discord reaction id resolution cases

* test: merge telegram reaction id cases

* test: merge audit resolved inspection cases

* test: merge audit gateway auth guardrail cases

* test: merge audit sandbox docker danger cases

* test: merge audit discord allowlist cases

* test: merge audit sandbox docker config cases

* test: merge audit browser sandbox cases

* test: merge audit allowCommands cases

* test: merge audit install metadata cases

* test: merge audit code safety cases

* test: merge audit dangerous flag cases

* test: merge audit trust exposure cases

* test: merge audit channel command hygiene cases

* test: merge slack validation cases

* test: merge update cli dry run cases

* test: merge update cli outcome cases

* test: merge update cli validation cases

* test: merge action media root cases

* test: merge update cli restart behavior cases

* test: merge update cli channel cases

* test: stabilize full gate

* feat(agents): infer image generation defaults

* docs(image-generation): document implicit tool enablement

* refactor: dedupe plugin lazy runtime helpers

* fix(telegram): persist sticky IPv4 fallback across polling restarts (fixes openclaw#48177) (openclaw#48282)

* fix(telegram): persist sticky IPv4 fallback across polling restarts (fixes openclaw#48177)

Hoist resolveTelegramTransport() out of createTelegramBot() so the
transport (and its sticky IPv4 fallback state) persists across polling
restarts. Previously, each polling restart created a new transport with
stickyIpv4FallbackEnabled=false, causing repeated IPv6 timeouts on
hosts with unstable IPv6 connectivity.

Changes:
- bot.ts: accept optional telegramTransport in TelegramBotOptions
- monitor.ts: resolve transport once before polling loop
- polling-session.ts: pass transport through to bot creation

AI-assisted (Claude Sonnet 4). Tested: tsc --noEmit clean.

* Update extensions/telegram/src/polling-session.ts

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

* style: fix oxfmt formatting in bot.ts

* test: cover telegram transport reuse across restarts

* fix: preserve telegram sticky IPv4 fallback across polling restarts (openclaw#48282) (thanks @yassinebkr)

---------

Co-authored-by: Yassine <yassinebkr@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* ACP: harden startup and move configured routing behind plugin seams (openclaw#48197)

* ACPX: keep plugin-local runtime installs out of dist

* Gateway: harden ACP startup and service PATH

* ACP: reinitialize error-state configured bindings

* ACP: classify pre-turn runtime failures as session init failures

* Plugins: move configured ACP routing behind channel seams

* Telegram tests: align startup probe assertions after rebase

* Discord: harden ACP configured binding recovery

* ACP: recover Discord bindings after stale runtime exits

* ACPX: replace dead sessions during ensure

* Discord: harden ACP binding recovery

* Discord: fix review follow-ups

* ACP bindings: load channel snapshots across workspaces

* ACP bindings: cache snapshot channel plugin resolution

* Experiments: add ACP pluginification holy grail plan

* Experiments: rename ACP pluginification plan doc

* Experiments: drop old ACP pluginification doc path

* ACP: move configured bindings behind plugin services

* Experiments: update bindings capability architecture plan

* Bindings: isolate configured binding routing and targets

* Discord tests: fix runtime env helper path

* Tests: fix channel binding CI regressions

* Tests: normalize ACP workspace assertion on Windows

* Bindings: isolate configured binding registry

* Bindings: finish configured binding cleanup

* Bindings: finish generic cleanup

* Bindings: align runtime approval callbacks

* ACP: delete residual bindings barrel

* Bindings: restore legacy compatibility

* Revert "Bindings: restore legacy compatibility"

This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe.

* Tests: drop ACP route legacy helper names

* Discord/ACP: fix binding regressions

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>

* feat: add bundled Chutes extension (openclaw#49136)

* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)

* docs(security): clarify wildcard Control UI origins

* refactor: clean extension api boundaries

* fix: remove duplicate setup helper imports

* fix(compaction): break safeguard cancel loop for sessions with no summarizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* feat(mattermost): add retry logic and timeout handling for DM channel creation (openclaw#42398)

Merged via squash.

Prepared head SHA: 3db47be
Co-authored-by: JonathanJing <17068507+JonathanJing@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm

* docs(hooks): clarify trust model and audit guidance

* test: stabilize memory async search close

* refactor: narrow extension public seams

* chore: add gog CLI support via custom Docker image

Add Dockerfile.gog to extend openclaw:local with the gog binary
(Google Workspace CLI). Also forward OPENAI_API_KEY and GEMINI_API_KEY
env vars in docker-compose.yml for both services.

Made-with: Cursor

* chore: inject GOG_KEYRING_PASSWORD for non-interactive gog auth

The gog CLI uses a file-based keyring to store Google OAuth tokens.
In Docker/headless environments, the keyring prompts for a passphrase
interactively, which blocks the agent from running gog autonomously.

Setting GOG_KEYRING_PASSWORD as an environment variable allows gog to
unlock the keyring without user interaction, enabling the agent to
execute Gmail, Calendar, Drive, and other Google Workspace commands
directly via the gog skill.

Setup steps to enable the gog skill in Docker:
1. Build the custom image: docker build -f Dockerfile.gog -t openclaw:gog .
2. Set OPENCLAW_IMAGE=openclaw:gog in .env
3. Set GOG_KEYRING_PASSWORD= in .env (empty = no passphrase)
4. Run: gog auth keyring file (inside container)
5. Run: gog auth credentials /path/to/client_secret.json
6. Run: gog auth add you@gmail.com --manual
7. Add to openclaw.json: tools.alsoAllow = ["exec", "process"]

Made-with: Cursor

* docs: add Docker commands reference and Mac migration plan

* docs: add docker save/load alternative to migration plan

* docs: add Docker+gog env example file

* config: clear unused OPENAI_API_KEY and document LLM auth in migration guide

* chore: remove .env from git tracking (contains secrets)

Made-with: Cursor

* docs: add openai-codex auth commands to migration guide

* docs: add gog token persistence, exec tool fix, and image migration cases

- docker-compose.yml: mount ~/.openclaw/gogcli as volume so gog tokens survive container restarts
- docs/docker-commands.html: add case 10 (gog token loss diagnosis + re-auth + volume persistence)
- docs/docker-commands.html: add case 11 (exec tool not enabled — tools.alsoAllow fix + persistence explanation)
- docs/docker-commands.html: add case 08 (migrate Docker Desktop images to Colima via docker save/load) + TOC entry

* docs: add Colima disk lock fix to docker-commands

* feat(docker): add local Whisper audio transcription to gog image

* docs: update docker-commands with TTS and contact number fixes

* docs: add gog token expiry troubleshooting and publish-app fix (caso 12)

* docs: add OpenAI Codex rate limit troubleshooting (caso 13)

* docs: add model set/status commands to caso 13

* docs: add provider/model switching guide (caso 14)

* docs: replace placeholder email with herbert.cadbury.bot@gmail.com

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
Co-authored-by: stim64045-spec <stim64045@gmail.com>
Co-authored-by: 大禹 <dayu@dayudeMac-mini.local>
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
Co-authored-by: Br1an <932039080@qq.com>
Co-authored-by: Br1an67 <29810238+Br1an67@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Co-authored-by: Stable Genius <stablegenius043@gmail.com>
Co-authored-by: stablegenius49 <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Chris Kimpton <chris@kimptoc.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sally O'Malley <somalley@redhat.com>
Co-authored-by: huntharo <harold@pwrdrvr.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: Andrew Demczuk <andrew.demczuk@gmail.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Jari Mustonen <jari.mustonen@iki.fi>
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: F_ool <112874572+hhhhao28@users.noreply.github.com>
Co-authored-by: Kwest OG <50209930+yassinebkr@users.noreply.github.com>
Co-authored-by: Yassine <yassinebkr@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Menglin Li <limenglin5911@gmail.com>
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: Jonathan Jing <JonathanJing@users.noreply.github.com>
Co-authored-by: JonathanJing <17068507+JonathanJing@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant