Skip to content

Refactor agent harness into plugin extension#63452

Closed
steipete wants to merge 301 commits into
mainfrom
app-server
Closed

Refactor agent harness into plugin extension#63452
steipete wants to merge 301 commits into
mainfrom
app-server

Conversation

@steipete

@steipete steipete commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a generic agent-harness registry / Plugin SDK surface and keep PI as the built-in fallback harness
  • move the Codex app-server runtime behind the bundled codex provider + harness extension
  • document harness plugins, Codex model refs, remaining refactor notes, and targeted verification

Testing

  • pnpm test src/agents/harness/selection.test.ts src/agents/harness/registry.test.ts src/agents/pi-embedded-runner/run/backend.test.ts extensions/codex/index.test.ts extensions/codex/provider.test.ts extensions/codex/app-server/dynamic-tools.test.ts extensions/codex/app-server/event-projector.test.ts
  • pnpm tsgo
  • pnpm plugin-sdk:api:check && pnpm run lint:plugins:plugin-sdk-subpaths-exported && pnpm run lint:extensions:no-relative-outside-package && pnpm run lint:extensions:no-src-outside-plugin-sdk && pnpm run lint:plugins:no-extension-test-core-imports && git diff --check
  • live smoke: Gateway + codex/gpt-5.4 returned CODEX-EXT-GPT54-OK
  • live smoke: Gateway model switch to codex/gpt-5.2 returned CODEX-EXT-GATEWAY-GPT52-OK
  • live smoke: direct/local codex/gpt-5.2 returned CODEX-EXT-GPT52-OK

Note: full pnpm test intentionally not run; this branch is verified with targeted tests per the app-server refactor plan.

@cursor

cursor Bot commented Apr 9, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces new plugin/SDK and gateway protocol surfaces (agent harness, commands.list, expanded agent/model fields) plus new macOS Talk Mode MLX TTS dependencies; these cross-cut core runtime and client contracts and could cause compatibility or build/runtime regressions if mismatched.

Overview
Adds a new plugin-facing agent harness surface and documents how provider plugins can pair with harness executors (including Codex model refs), alongside new docs for Active Memory and other updated testing/CI guidance.

Extends gateway/native client contracts by adding commands.list response models, enriching agent create/update payloads (model/emoji) and model catalog entries (alias), and surfacing errorKind on ChatEvent.

Tightens automation and native app behavior by adding a CI check:import-cycles guard, expanding macOS host-env sanitization policy, and adding an experimental macOS MLX-based Talk Mode TTS path (with new Swift dependencies and provider selection), plus routine version bumps/release notes for Android/iOS/macOS and minor labeler updates.

Reviewed by Cursor Bugbot for commit cdfe60c. Bugbot is set up for automated code reviews on this repo. Configure here.

@aisle-research-bot

aisle-research-bot Bot commented Apr 9, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High Environment-controlled external process spawn enables unintended code execution (Codex app-server client)
2 🟠 High Approval request not scoped to thread/turn when request params are missing
3 🟠 High Codex app-server runs with approvals disabled by default and sandbox mode decoupled from OpenClaw sandbox context
4 🟠 High Untrusted plugins can register high-privilege AgentHarness implementations (agent runtime hijack)
5 🟡 Medium Auto-mode harness fallback can replay side effects by rerunning attempt on PI after plugin harness error
6 🟡 Medium Potential sensitive data exposure via logging of spawned process stderr
7 🟡 Medium Potential symlink/hardlink overwrite via non-atomic write of Codex app-server binding file
8 🟡 Medium Synthetic auth for Codex provider uses hard-coded token enabling unintended authenticated state and outbound requests
9 🟡 Medium Unbounded transcript read/split under session write lock can cause memory/CPU DoS
10 🟡 Medium Codex dynamic tool bridge reports success even when AgentToolResult encodes failure
1. 🟠 Environment-controlled external process spawn enables unintended code execution (Codex app-server client)
Property Value
Severity High
CWE CWE-78
Location extensions/codex/app-server/client.ts:93-100

Description

The Codex app-server client spawns an external executable whose path and arguments are controlled by environment variables:

  • OPENCLAW_CODEX_APP_SERVER_BIN controls the executable name/path (defaults to codex and therefore searches PATH).
  • OPENCLAW_CODEX_APP_SERVER_ARGS controls the argument vector.
  • The child process is launched with env: process.env, inheriting the full environment.

If an attacker can influence environment variables or PATH in the runtime (e.g., multi-tenant worker, CI, plugin host, or any scenario where untrusted users can affect the execution environment), they can:

  • Execute an arbitrary binary by setting OPENCLAW_CODEX_APP_SERVER_BIN to a malicious path, or by placing a malicious codex earlier in PATH.
  • Alter behavior via attacker-controlled args.

Vulnerable code:

const bin = process.env.OPENCLAW_CODEX_APP_SERVER_BIN?.trim() || "codex";
const extraArgs = splitShellWords(process.env.OPENCLAW_CODEX_APP_SERVER_ARGS ?? "");
const args = extraArgs.length > 0 ? extraArgs : ["app-server", "--listen", "stdio://"];
const child = spawn(bin, args, {
  env: process.env,
  stdio: ["pipe", "pipe", "pipe"],
});

Recommendation

Treat the spawned binary as a trusted dependency and prevent untrusted env/PATH from selecting it.

Recommended mitigations (pick what fits your threat model):

  1. Require an absolute, allowlisted path (or ship/resolve a bundled binary):
import path from "node:path";

const configured = process.env.OPENCLAW_CODEX_APP_SERVER_BIN?.trim();
const bin = configured ? path.resolve(configured) : "/usr/local/bin/codex";
if (!bin.startsWith("/usr/local/bin/")) {
  throw new Error("Invalid OPENCLAW_CODEX_APP_SERVER_BIN");
}
  1. Remove/limit env-controlled args; if customization is needed, validate against an allowlist of supported flags.

  2. Do not pass the full environment. Pass only required variables:

const env = {
  PATH: process.env.PATH, // if needed
  HOME: process.env.HOME,// explicitly required OpenAI/Codex vars only
};
spawn(bin, args, { env, stdio: ["pipe","pipe","pipe"] });
  1. In high-risk deployments, consider a configuration file with strict permissions instead of environment variables.
2. 🟠 Approval request not scoped to thread/turn when request params are missing
Property Value
Severity High
CWE CWE-862
Location extensions/codex/app-server/approval-bridge.ts:172-189

Description

The Codex app-server approval bridge only checks that an incoming approval request matches the current thread/turn when identifiers are present. If requestParams is missing or not a JSON object, matchesCurrentTurn() returns true, and the bridge proceeds to request a gateway approval and returns an approval decision for the current run.

This creates an authorization/scoping gap:

  • Input: requestParams originates from the codex app-server RPC request (request.params).
  • Check bypass: If params is absent or non-object, the bridge treats it as matching the current turn.
  • Effect/sink: The bridge calls callGatewayTool("plugin.approval.request", ...) and returns an approval response to the app-server, potentially granting approvals to an unscoped request.

Vulnerable code:

function matchesCurrentTurn(requestParams: JsonObject | undefined, threadId: string, turnId: string): boolean {
  if (!requestParams) {
    return true;
  }
  ...
}

If the app-server (or a compromised/malicious binary launched via OPENCLAW_CODEX_APP_SERVER_BIN) can emit an approval request without thread/turn identifiers, it can still obtain approvals from the user/guardian for the current run, even though the request cannot be tied to a specific turn. This undermines the intended per-turn approval binding and may allow approval confusion or replay across turns within the same connection.

Recommendation

Require approval requests to be explicitly scoped to the current thread/turn, and reject requests that omit identifiers.

Example fix:

function matchesCurrentTurn(
  requestParams: JsonObject | undefined,
  threadId: string,
  turnId: string,
): boolean {
  if (!requestParams) return false;

  const requestThreadId =
    readString(requestParams, "threadId") ?? readString(requestParams, "conversationId");
  const requestTurnId = readString(requestParams, "turnId");// Require both identifiers to be present.
  if (!requestThreadId || !requestTurnId) return false;

  return requestThreadId === threadId && requestTurnId === turnId;
}

Additionally, consider logging/telemetry when rejecting unscoped approval requests so unexpected app-server behavior is visible.

3. 🟠 Codex app-server runs with approvals disabled by default and sandbox mode decoupled from OpenClaw sandbox context
Property Value
Severity High
CWE CWE-863
Location extensions/codex/app-server/run-attempt.ts:460-474

Description

In runCodexAppServerAttempt, the Codex app-server is started with an approval policy that defaults to "never" and a sandbox mode chosen only from environment variables, not from the already-resolved OpenClaw sandbox context.

Impacts:

  • Approval bypass / missing authorization: when approvalPolicy is "never", the app-server may execute privileged side-effect actions (e.g., commandExecution, fileChange) without ever emitting */requestApproval RPCs, so the OpenClaw approval bridge (handleCodexAppServerApprovalRequest) is never invoked.
  • Sandbox policy mismatch / privilege escalation: OpenClaw resolves a sandbox context via resolveSandboxContext(...), but thread/start sends sandbox: resolveAppServerSandbox() sourced from OPENCLAW_CODEX_APP_SERVER_SANDBOX (default "workspace-write"). This can silently grant the app-server more filesystem/network access than the OpenClaw run’s sandbox intends (e.g., OpenClaw read-only, but app-server workspace-write; or danger-full-access if env set).

Vulnerable code (defaults):

function resolveAppServerApprovalPolicy(): "never" | "on-request" | "on-failure" | "untrusted" {
  const raw = process.env.OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY?.trim();
  if (raw === "on-request" || raw === "on-failure" || raw === "untrusted") {
    return raw;
  }
  return "never";
}

function resolveAppServerSandbox(): "read-only" | "workspace-write" | "danger-full-access" {
  const raw = process.env.OPENCLAW_CODEX_APP_SERVER_SANDBOX?.trim();
  if (raw === "read-only" || raw === "danger-full-access") {
    return raw;
  }
  return "workspace-write";
}

And the values are used when starting the app-server thread/turn:

  • turn/start: approvalPolicy: resolveAppServerApprovalPolicy()
  • thread/start: approvalPolicy: resolveAppServerApprovalPolicy() and sandbox: resolveAppServerSandbox()

Recommendation

Tie Codex app-server policy to the already-resolved OpenClaw sandbox/approval configuration and make secure behavior the default.

  1. Default to an approval-requiring policy (e.g. "on-request" or "untrusted") unless explicitly configured otherwise.
  2. Derive app-server sandbox mode from resolveSandboxContext(...), not only from environment variables, and never allow danger-full-access unless an explicit, trusted config enables it.

Example:

function resolveAppServerApprovalPolicy(params: EmbeddedRunAttemptParams) {// Prefer explicit OpenClaw config, and default to requiring approvals
  return (process.env.OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY as any) ?? "on-request";
}

function resolveAppServerSandboxFromContext(
  sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>,
): "read-only" | "workspace-write" {
  if (!sandbox?.enabled) return "read-only";
  return sandbox.workspaceAccess === "rw" ? "workspace-write" : "read-only";
}// When starting:// approvalPolicy: resolveAppServerApprovalPolicy(params),// sandbox: resolveAppServerSandboxFromContext(sandbox),

Additionally, consider rejecting startup if env requests danger-full-access while OpenClaw sandbox is enabled/restricted, to prevent accidental privilege escalation.

4. 🟠 Untrusted plugins can register high-privilege AgentHarness implementations (agent runtime hijack)
Property Value
Severity High
CWE CWE-285
Location src/plugins/registry.ts:714-761

Description

The plugin API now exposes api.registerAgentHarness(...), which lets any loaded plugin register an AgentHarness implementation. The harness is then considered during runtime selection and can be chosen (e.g., by returning supported: true with a high priority).

This is a high-privilege surface:

  • A harness controls low-level agent execution (runAttempt, optional compact, reset).
  • It can observe/modify prompts, tool calls, workspace/session file paths, and outputs.
  • Registration is not gated to bundled/trusted plugins, nor is there a permission/allowlist check at registration time.

As a result, if third-party/untrusted plugins are allowed, a malicious plugin can register a harness that intercepts all agent runs and exfiltrates data or executes unintended actions.

Vulnerable code:

// src/plugins/registry.ts
registerAgentHarness: (harness) => registerAgentHarness(record, harness),

and the registration implementation performs only ID uniqueness checks, then registers globally.

Recommendation

Gate agent harness registration to trusted/bundled plugins (or an explicit admin allowlist) and enforce it in code.

Options:

  1. Manifest permission + enforcement

    • Add a manifest flag like capabilities: { agentHarness: true } (default false).
    • In registerAgentHarness, reject unless the plugin manifest grants it.
  2. Bundled-only enforcement

    • Reject registerAgentHarness for plugins whose source/origin indicates non-bundled installation.
  3. Config allowlist

    • Require config.plugins.allowAgentHarness: ["plugin-id"] and check it at registration.

Example (conceptual) enforcement in src/plugins/registry.ts:

const registerAgentHarness = (record: PluginRecord, harness: AgentHarness) => {
  if (!record.manifest?.capabilities?.agentHarness) {
    pushDiagnostic({
      level: "error",
      pluginId: record.id,
      source: record.source,
      message: "agent harness registration is not permitted for this plugin",
    });
    return;
  }// proceed with registration...
};

Also consider:

  • Disallowing priority above a safe ceiling for non-core harnesses.
  • Preventing supports() from claiming broad coverage unless explicitly configured.
  • Logging and surfacing registered harnesses prominently to operators.
5. 🟡 Auto-mode harness fallback can replay side effects by rerunning attempt on PI after plugin harness error
Property Value
Severity Medium
CWE CWE-362
Location src/agents/harness/selection.ts:78-86

Description

runAgentHarnessAttemptWithFallback reruns the same attempt through the embedded PI harness when a selected (non-PI) harness throws in auto mode.

  • A non-PI harness may have already executed side-effecting actions (e.g., sending messages via messaging tools, writing files, external actions) before throwing.
  • The fallback logic does not check any "side effects already happened" indicator (e.g., replayMetadata.hadPotentialSideEffects, tool telemetry such as didSendViaMessagingTool, etc.) before rerunning.
  • This can lead to duplicate/replayed actions (replay/TOCTOU-style bug) whenever the first harness partially succeeds then errors.

Vulnerable code:

try {
  return await harness.runAttempt(params);
} catch (error) {
  if (runtime !== "auto") {
    throw error;
  }
  log.warn(`${harness.label} failed; falling back to embedded PI backend`, { error });
  return createPiAgentHarness().runAttempt(params);
}

Recommendation

Gate fallback on an explicit replay safety signal, and default to no fallback when uncertain.

Options:

  1. Require harnesses to throw a structured error that includes whether side effects may have occurred.
  2. Or require harnesses to return an EmbeddedRunAttemptResult even on failure (with replayMetadata.hadPotentialSideEffects set) and only fallback if replayMetadata.replaySafe === true.
  3. Or track side effects centrally (e.g., via shared tool telemetry/event bus) and block fallback once any side-effecting tool is invoked.

Example (conceptual) gating:

try {
  return await harness.runAttempt(params);
} catch (err) {
  if (runtime !== "auto") throw err;

  const replaySafe = isReplaySafeError(err); // must be false/unknown by default
  if (!replaySafe) {// avoid duplicate side effects
    throw err;
  }

  return createPiAgentHarness().runAttempt(params);
}
6. 🟡 Potential sensitive data exposure via logging of spawned process stderr
Property Value
Severity Medium
CWE CWE-532
Location extensions/codex/app-server/client.ts:75-80

Description

The Codex app-server client logs the spawned process's stderr verbatim at debug level:

  • Any secrets, tokens, or file contents that the codex process writes to stderr will be copied into OpenClaw logs.
  • In practice, CLIs and SDKs may print API keys, auth headers, stack traces containing environment variables, or request payloads on failures.

Vulnerable code:

child.stderr.on("data", (chunk) => {
  const text = chunk.toString("utf8").trim();
  if (text) {
    embeddedAgentLog.debug(`codex app-server stderr: ${text}`);
  }
});

Whether this is exploitable depends on your logging configuration and who can access logs, but it is a common source of credential leakage in CI/hosted environments.

Recommendation

Avoid logging untrusted stderr verbatim, or ensure it is redacted and gated.

Options:

  • Remove the log, or log only metadata (e.g., byte length / exit code).
  • Redact known secret patterns before logging (API keys, bearer tokens, etc.).
  • Gate behind an explicit dev-only flag and ensure production/CI default doesn’t emit it.

Example redaction:

function redact(s: string): string {
  return s
    .replace(/(Bearer\s+)[A-Za-z0-9._-]+/g, "$1<redacted>")
    .replace(/(OPENAI_API_KEY=)[^\s]+/g, "$1<redacted>");
}

child.stderr.on("data", (chunk) => {
  const text = chunk.toString("utf8").trim();
  if (text && process.env.OPENCLAW_DEBUG_CODEX_STDERR === "1") {
    embeddedAgentLog.debug(`codex app-server stderr: ${redact(text)}`);
  }
});
7. 🟡 Potential symlink/hardlink overwrite via non-atomic write of Codex app-server binding file
Property Value
Severity Medium
CWE CWE-59
Location extensions/codex/app-server/session-binding.ts:72-75

Description

writeCodexAppServerBinding() writes a JSON sidecar to a path derived directly from sessionFile using fs.writeFile(), and clearCodexAppServerBinding() deletes it with fs.unlink().

Even though sessionFile is generally expected to be under the sessions directory, these operations:

  • are not atomic (can be partially written/corrupted if concurrent writers exist)
  • follow symlinks by default

If an attacker (or untrusted local process) can create/replace ${sessionFile}.codex-app-server.json with a symlink/hardlink pointing to another file writable by the OpenClaw process, fs.writeFile() can overwrite that target.

Vulnerable code:

await fs.writeFile(
  resolveCodexAppServerBindingPath(sessionFile),
  `${JSON.stringify(payload, null, 2)}\n`,
);

Recommendation

Harden binding file writes/deletes against symlink attacks and corruption:

  • Write atomically to a temp file in the same directory and rename() it into place
  • Ensure the destination is a regular file (not a symlink) before replacing
  • Optionally use open() with O_NOFOLLOW (where supported) / validate with lstat()
  • Consider coordinating with the existing session write lock (same lock used for transcript updates)

Example (atomic replace with basic symlink check):

import path from "node:path";
import fs from "node:fs/promises";

const bindingPath = resolveCodexAppServerBindingPath(sessionFile);
const dir = path.dirname(bindingPath);
const tmpPath = path.join(dir, `.tmp-${path.basename(bindingPath)}-${process.pid}`);// Refuse to overwrite if an existing path is a symlink
try {
  const st = await fs.lstat(bindingPath);
  if (st.isSymbolicLink()) throw new Error("binding path is a symlink");
} catch (e: any) {
  if (e?.code !== "ENOENT") throw e;
}

await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
await fs.rename(tmpPath, bindingPath);

For deletion, similarly lstat() before unlinking, or remove only if it is a regular file you created.

8. 🟡 Synthetic auth for Codex provider uses hard-coded token enabling unintended authenticated state and outbound requests
Property Value
Severity Medium
CWE CWE-287
Location extensions/codex/provider.ts:87-91

Description

The new Codex provider plugin declares auth: [] (no user-configured credentials) but also returns a hard-coded synthetic token via resolveSyntheticAuth. Core discovery code uses synthetic auth to inject credentials into the embedded PI model registry, which can mark the provider as "available" even when the user is not actually logged in.

Impact:

  • Misleading availability / auth bypass in selection logic: provider may appear usable because a credential entry exists.
  • Unintended outbound requests & potential data exposure: because the provider catalog uses a remote base URL (https://chatgpt.com/backend-api) with auth: "token", downstream code paths may send requests (including user prompts or metadata) with Authorization: Bearer codex-app-server even when the user did not configure/consent to remote auth.

Vulnerable code:

resolveSyntheticAuth: () => ({
  apiKey: "codex-app-server",
  source: "codex-app-server",
  mode: "token",
}),

Related data-flow:

  • Synthetic auth providers are enumerated (resolveRuntimeSyntheticAuthProviderRefs)
  • Discovery injects returned apiKey into PI credentials (resolvePiCredentialsForDiscovery) which can cause providers to be treated as authenticated/available during model discovery/selection.

Recommendation

Do not return a usable-looking token unless it is derived from verified local Codex app-server/session state.

Options:

  1. Gate synthetic auth behind a real session check (e.g., ask the Codex app-server whether the user is logged in), and return undefined when not authenticated.
  2. If synthetic auth is only meant to unblock local harness operation, use a non-secret marker that cannot be mistaken for a real token and is not used to form outbound Authorization headers.

Example (gated):

resolveSyntheticAuth: async (ctx) => {
  const session = await codexAppServerGetSession();
  if (!session?.token) return undefined;
  return { apiKey: session.token, source: "codex-app-server", mode: "token" };
}

Also consider setting provider baseUrl to a local endpoint (if applicable) when using synthetic auth owned by the harness, or ensure request code refuses to send prompts when auth is synthetic/placeholder.

9. 🟡 Unbounded transcript read/split under session write lock can cause memory/CPU DoS
Property Value
Severity Medium
CWE CWE-400
Location extensions/codex/app-server/transcript-mirror.ts:53-75

Description

mirrorCodexAppServerTranscript() acquires a session write lock and then calls readTranscriptIdempotencyKeys(), which reads the entire session transcript into memory and splits it into lines.

This can lead to denial of service if a transcript grows large (intentionally or naturally):

  • fs.readFile(sessionFile, "utf8") loads the full file into memory
  • raw.split(/\r?\n/) creates a large array of strings (high memory overhead)
  • JSON.parse is attempted on every line
  • This work happens while holding the session write lock, blocking other operations on that session for up to the lock timeout/hold duration

Vulnerable code:

raw = await fs.readFile(sessionFile, "utf8");
for (const line of raw.split(/\r?\n/)) {
  ... JSON.parse(line) ...
}

Recommendation

Avoid reading/parsing the full transcript under the write lock.

Mitigations:

  1. Stream the file line-by-line with a hard cap (bytes/lines) and/or only scan a recent tail window.
  2. Reduce lock contention by:
    • collecting idempotency keys outside the write lock (accepting minor races), or
    • storing idempotency keys in a separate small index file, or
    • using SessionManager/transcript format features that can query idempotency keys without full scan.

Example using streaming with a size cap (conceptual):

import { createReadStream } from "node:fs";
import readline from "node:readline";

async function readKeysCapped(file: string, maxLines = 50_000): Promise<Set<string>> {
  const keys = new Set<string>();
  const rl = readline.createInterface({ input: createReadStream(file, { encoding: "utf8" }) });
  let lines = 0;
  for await (const line of rl) {
    if (++lines > maxLines) break;// parse line and collect idempotencyKey
  }
  return keys;
}

Also consider bounding messages mirrored per call to prevent rapid transcript growth.

10. 🟡 Codex dynamic tool bridge reports success even when AgentToolResult encodes failure
Property Value
Severity Medium
CWE CWE-252
Location extensions/codex/app-server/dynamic-tools.ts:65-76

Description

createCodexDynamicToolBridge().handleToolCall() treats any resolved tool.execute(...) as success and returns success: true to the Codex app-server, without inspecting the returned AgentToolResult for failure indicators.

This is problematic because OpenClaw tool execution can return an error result without throwing. For example, src/agents/pi-tool-definition-adapter.ts catches tool exceptions and returns an AgentToolResult with details.status: "error" (instead of rethrowing) via buildToolExecutionErrorResult().

Impact:

  • The Codex app-server may believe a tool call succeeded even when it failed, leading to inconsistent state.
  • Safety/approval policies that rely on accurate failure reporting (e.g. approvalPolicy: "on-failure") may not trigger as intended, potentially bypassing intended interlocks.
  • Side-effecting tools (messaging, cron, exec-like integrations) may be retried or followed up incorrectly because failures are masked as successes.

Vulnerable code:

const result = await tool.execute(call.callId, preparedArgs, params.signal);
...
return {
  contentItems: result.content.flatMap(convertToolContent),
  success: true,
};

Recommendation

Derive the response success from the returned AgentToolResult, not solely from whether execute() threw.

Suggested approach:

  1. Define a small predicate that detects failure results (e.g. details.status === "error" or details.status === "failed", and/or other canonical fields used by your tools).
  2. Set success accordingly.
  3. Optionally, include a normalized error message in contentItems when the tool returned a failure result.

Example:

function isFailureResult(result: AgentToolResult<unknown>): boolean {
  const details = (result as { details?: unknown }).details;
  if (details && typeof details === "object" && !Array.isArray(details)) {
    const status = (details as Record<string, unknown>).status;
    return status === "error" || status === "failed";
  }
  return false;
}

const result = await tool.execute(call.callId, preparedArgs, params.signal);
const failed = isFailureResult(result);
return {
  contentItems: result.content.flatMap(convertToolContent),
  success: !failed,
};

Also consider updating telemetry (successfulCronAdds, messaging side-effects) to only record “success” when the tool result indicates success, not merely when no exception was thrown.


Analyzed PR: #63452 at commit fdcd766

Last updated on: 2026-04-09T02:52:52Z

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime scripts Repository scripts agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Apr 9, 2026
@greptile-apps

greptile-apps Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extracts the Codex app-server runtime into a proper bundled plugin extension (extensions/codex) and introduces a generic AgentHarness registry/selection layer in core, with PI remaining as the built-in fallback harness. The new openclaw/plugin-sdk/agent-harness subpath exposes the public contract for third-party harness plugins.

  • Silent PI fallback on forced runtime miss (src/agents/harness/selection.ts lines 31-37): when OPENCLAW_AGENT_RUNTIME is set to a specific harness ID (e.g. \"codex\") but the corresponding plugin is not registered, the selection function silently returns the PI harness with no warning — unlike the auto-with-fallback path which does log. An operator debugging a Codex plugin load failure would have no indication their configured runtime was ignored.

Confidence Score: 4/5

Safe to merge after addressing the silent fallback in the forced-runtime path; all other changes are well-structured.

One P1 finding: when OPENCLAW_AGENT_RUNTIME is set to a harness ID that is not registered (e.g. plugin load failure), selectAgentHarness silently returns PI with no diagnostic, causing the operator to run on an unexpected backend. All other logic — registry, event projector, tool bridge, session binding, SDK surface — is sound and the targeted tests cover the key selection scenarios.

src/agents/harness/selection.ts — the forced-runtime fallback branch needs a warning log before falling back to PI.

Vulnerabilities

No security concerns identified. The Codex app-server binding files are session-scoped and path-validated. Sandbox and approval-policy resolution follow existing patterns. No secrets, credentials, or auth tokens are exposed through the new SDK surface.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/harness/selection.ts
Line: 31-37

Comment:
**Silent PI fallback when forced runtime ID is not registered**

When `OPENCLAW_AGENT_RUNTIME` is set to a specific harness ID (e.g. `"codex"`) but that harness is not yet registered — because the plugin failed to load or was disabled — `selectAgentHarness` silently returns the PI harness with no log or diagnostic. The operator asked for a specific backend and instead gets PI behavior with no indication anything went wrong. Compare the `auto`-with-fallback path at line 79 which does emit a `log.warn`; the forced path should do the same (or throw).

```suggestion
  if (runtime !== "auto") {
    const forced = harnesses.find((entry) => entry.id === runtime);
    if (forced) {
      return forced;
    }
    log.warn(
      `forced agent runtime "${runtime}" is not registered; falling back to embedded PI backend`,
    );
    return createPiAgentHarness();
  }
```

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/harness/registry.ts
Line: 1

Comment:
**Registry uses PI-specific logger**

`registry.ts` is generic harness infrastructure, but it imports its logger from `"../pi-embedded-runner/logger.js"` — a PI implementation detail. If the logger namespace or module ever moves, the generic registry will need updating too. Consider importing from a shared logger factory (e.g. `createSubsystemLogger("agents")`) to decouple the registry from the PI runner.

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

Reviews (1): Last reviewed commit: "docs: document agent harness plugin SDK" | Re-trigger Greptile

Comment on lines +31 to +37
if (runtime !== "auto") {
const forced = harnesses.find((entry) => entry.id === runtime);
if (forced) {
return forced;
}
return createPiAgentHarness();
}

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.

P1 Silent PI fallback when forced runtime ID is not registered

When OPENCLAW_AGENT_RUNTIME is set to a specific harness ID (e.g. "codex") but that harness is not yet registered — because the plugin failed to load or was disabled — selectAgentHarness silently returns the PI harness with no log or diagnostic. The operator asked for a specific backend and instead gets PI behavior with no indication anything went wrong. Compare the auto-with-fallback path at line 79 which does emit a log.warn; the forced path should do the same (or throw).

Suggested change
if (runtime !== "auto") {
const forced = harnesses.find((entry) => entry.id === runtime);
if (forced) {
return forced;
}
return createPiAgentHarness();
}
if (runtime !== "auto") {
const forced = harnesses.find((entry) => entry.id === runtime);
if (forced) {
return forced;
}
log.warn(
`forced agent runtime "${runtime}" is not registered; falling back to embedded PI backend`,
);
return createPiAgentHarness();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/selection.ts
Line: 31-37

Comment:
**Silent PI fallback when forced runtime ID is not registered**

When `OPENCLAW_AGENT_RUNTIME` is set to a specific harness ID (e.g. `"codex"`) but that harness is not yet registered — because the plugin failed to load or was disabled — `selectAgentHarness` silently returns the PI harness with no log or diagnostic. The operator asked for a specific backend and instead gets PI behavior with no indication anything went wrong. Compare the `auto`-with-fallback path at line 79 which does emit a `log.warn`; the forced path should do the same (or throw).

```suggestion
  if (runtime !== "auto") {
    const forced = harnesses.find((entry) => entry.id === runtime);
    if (forced) {
      return forced;
    }
    log.warn(
      `forced agent runtime "${runtime}" is not registered; falling back to embedded PI backend`,
    );
    return createPiAgentHarness();
  }
```

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

Comment thread src/agents/harness/registry.ts Outdated
@@ -0,0 +1,81 @@
import { log } from "../pi-embedded-runner/logger.js";

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.

P2 Registry uses PI-specific logger

registry.ts is generic harness infrastructure, but it imports its logger from "../pi-embedded-runner/logger.js" — a PI implementation detail. If the logger namespace or module ever moves, the generic registry will need updating too. Consider importing from a shared logger factory (e.g. createSubsystemLogger("agents")) to decouple the registry from the PI runner.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/registry.ts
Line: 1

Comment:
**Registry uses PI-specific logger**

`registry.ts` is generic harness infrastructure, but it imports its logger from `"../pi-embedded-runner/logger.js"` — a PI implementation detail. If the logger namespace or module ever moves, the generic registry will need updating too. Consider importing from a shared logger factory (e.g. `createSubsystemLogger("agents")`) to decouple the registry from the PI runner.

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

Comment thread extensions/codex/app-server/run-attempt.ts Outdated
Comment thread extensions/codex/app-server/client.ts Outdated
Comment thread extensions/codex/provider.ts
Comment thread extensions/codex/provider.ts

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b36855f1b

ℹ️ 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 +123 to +125
completed = true;
resolveCompletion?.();
}

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.

P1 Badge Initialize completion resolver before handling turn notifications

turn/completed notifications can be processed before resolveCompletion is assigned, because the notification handler is registered earlier and calls resolveCompletion?.() opportunistically. When that race happens (for very fast turns), the resolver call is dropped and the attempt waits until timeout even though the turn already finished, producing spurious timeouts/interrupts.

Useful? React with 👍 / 👎.

Comment on lines +69 to +73
params.abortSignal?.addEventListener(
"abort",
() => {
runAbortController.abort(params.abortSignal?.reason ?? "upstream_abort");
},

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.

P2 Badge Propagate already-aborted upstream signals immediately

This only subscribes to future abort events and never checks whether params.abortSignal is already aborted when the attempt starts. In that case the local controller stays active, so the Codex turn can still start and run until another timeout/abort path fires instead of stopping immediately.

Useful? React with 👍 / 👎.

Comment on lines +253 to +257
this.closed = true;
for (const pending of this.pending.values()) {
pending.reject(error);
}
this.pending.clear();

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.

P2 Badge Reset shared Codex client after transport shutdown

When the app-server process exits/errors, closeWithError marks the client closed but leaves the module-level shared client promise intact. Subsequent getSharedCodexAppServerClient() calls can keep returning that closed client, causing every later request to fail until process restart instead of recreating the transport.

Useful? React with 👍 / 👎.

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca14b2fdd8

ℹ️ 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 +310 to +312
const response = await params.client.request<CodexThreadResumeResponse>("thread/resume", {
threadId: binding.threadId,
});

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.

P2 Badge Recreate thread when dynamic tool config changes

buildDynamicTools derives the tool list from per-attempt flags (disableTools, toolsAllow, model/tool support), but the resume path ignores that fresh list and immediately calls thread/resume with only the old threadId. Since dynamicTools are only provided on thread/start, resumed sessions keep the first turn’s tool configuration, so later turns can lose newly allowed tools (or keep stale schemas) after config/model changes. This can make subsequent attempts behave incorrectly until the binding is cleared.

Useful? React with 👍 / 👎.

Comment thread src/plugins/registry.ts Outdated
id,
pluginId: harness.pluginId ?? record.id,
};
registerGlobalAgentHarness(normalizedHarness, { ownerPluginId: record.id });

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.

P2 Badge Respect non-activating mode for harness registration

registerAgentHarness always mutates the process-global harness registry, even when the registry is created with activateGlobalSideEffects: false. Non-activating loads (such as metadata/CLI registry reads) are expected to stay local, so this leaks harnesses into global state and can change later harness selection or trigger duplicate-registration errors on subsequent activating loads in the same process.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added the docker Docker and sandbox tooling label Apr 9, 2026
Comment thread extensions/codex/provider.ts
replayMetadata: {
hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool,
replaySafe: !toolTelemetry.didSendViaMessagingTool,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Replay metadata ignores mutating tools and cron side effects

High Severity

The Codex event projector's replayMetadata only considers didSendViaMessagingTool, but the PI harness equivalent (buildAttemptReplayMetadata) also checks toolMetas for mutating tool names (like bash, apply_patch) and successfulCronAdds. Native Codex command executions and file changes are recorded in toolMetas by recordToolMeta, and cron adds are tracked in toolTelemetry.successfulCronAdds, but neither is consulted when computing hadPotentialSideEffects / replaySafe. This can cause the system to consider a turn replay-safe even after it executed shell commands, applied patches, or added cron jobs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5111de9. Configure here.

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5111de9e83

ℹ️ 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 +163 to +165
replayMetadata: {
hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool,
replaySafe: !toolTelemetry.didSendViaMessagingTool,

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.

P1 Badge Mark mutating Codex turns as having side effects

replayMetadata is currently derived only from didSendViaMessagingTool, so turns that run mutating tools (for example commandExecution, fileChange, or successful cron add) but do not send a message are misclassified as replay-safe. In those cases, the incomplete-turn/retry safeguards in src/agents/pi-embedded-runner/run/incomplete-turn.ts can surface the safer "try again" path without the side-effect warning even though actions may already have executed. This should include non-messaging side effects (at least tool-mutation and cron signals, consistent with buildAttemptReplayMetadata).

Useful? React with 👍 / 👎.

if (!this.child.killed) {
this.child.kill?.();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Client close() leaks pending requests and stale singleton

High Severity

The close() method sets this.closed = true before terminating the child process. This prevents the exit handler from fully cleaning up via closeWithError(), leaving pending requests unresolved and the shared client singleton uncleared.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdcd766. Configure here.

embeddedAgentLog.warn("codex app-server notification handler failed", { error });
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fire-and-forget async notification handlers cause data races

Medium Severity

handleNotification in the client fires async notification handlers without awaiting them. The run-attempt registers an async handler that updates the CodexAppServerEventProjector state and resolves the completion promise. When multiple notifications arrive in rapid succession, they process concurrently on the projector, risking interleaved state mutations—e.g., turn/completed can resolve the completion promise before a preceding item/agentMessage/delta handler finishes, causing the final result to miss assistant text.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdcd766. Configure here.

(runtime === "auto" && (provider === "codex" || provider === "openai-codex"));
if (!shouldUseCodex) {
return undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Compact function duplicates harness selection runtime logic

Low Severity

maybeCompactCodexAppServerSession independently calls resolveEmbeddedAgentRuntime() and re-checks the provider, duplicating logic that selectAgentHarness in selection.ts already performed before invoking this function through the harness's compact callback. These two decision paths could diverge silently if the provider-matching logic is updated in one place but not the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdcd766. Configure here.

if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
return value as JsonObject;

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 coerceJsonObject function is never used anywhere

Low Severity

coerceJsonObject is exported from protocol.ts but is never imported or called anywhere in the codebase. It's dead code that adds unnecessary surface area to the module.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdcd766. Configure here.

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fdcd7660c0

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

"providers": ["codex"],
"modelSupport": {
"providerIds": ["codex"],
"modelPrefixes": ["gpt-", "o1", "o3", "o4", "arcanine"]

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.

P2 Badge Remove ambiguous Codex model ownership prefixes

This manifest claims broad shorthand prefixes (gpt-, o1, o3, o4) that already belong to the bundled OpenAI plugin, so shorthand model refs now match multiple bundled owners. In src/plugins/providers.ts, resolvePreferredManifestPluginIds returns undefined for ambiguous bundled matches, and then resolveConfiguredPluginAutoEnableCandidates in src/config/plugin-auto-enable.shared.ts skips auto-enable unless there is exactly one owner. The regression is that configs relying on model-based plugin activation (especially constrained allowlist/disabled setups) can stop auto-enabling the right provider plugin for refs like gpt-5.4.

Useful? React with 👍 / 👎.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate "talk config" prefix in log message
    • Removed the duplicate 'talk config' prefix from line 925 so the log message now reads as a single coherent line without repetition.

Create PR

Or push these changes by commenting:

@cursor push 68eb36a449
Preview (68eb36a449)
diff --git a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
--- a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
+++ b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
@@ -922,7 +922,7 @@
         self.logger
             .info(
                 "talk config provider=\(cfg.activeProvider, privacy: .public) " +
-                    "talk config voiceId=\(voiceLabel, privacy: .public) " +
+                    "voiceId=\(voiceLabel, privacy: .public) " +
                     "modelId=\(modelLabel, privacy: .public) " +
                     "apiKey=\(hasApiKey, privacy: .public) " +
                     "interrupt=\(cfg.interruptOnSpeech, privacy: .public) " +

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cdfe60c. Configure here.

.info(
"talk config voiceId=\(voiceLabel, privacy: .public) " +
"talk config provider=\(cfg.activeProvider, privacy: .public) " +
"talk config voiceId=\(voiceLabel, privacy: .public) " +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate "talk config" prefix in log message

Low Severity

The log message in reloadConfig() contains a duplicate "talk config" prefix. The concatenated string reads "talk config provider=... talk config voiceId=..." because when "provider=..." was prepended to the existing log line, the original "talk config " before "voiceId" was not removed. The second occurrence is a copy-paste artifact.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cdfe60c. Configure here.

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdfe60cd42

ℹ️ 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 thread src/plugins/registry.ts
Comment on lines +507 to +510
const id = harness.id.trim();
if (!id) {
pushDiagnostic({
level: "error",

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.

P2 Badge Reserve pi harness id for built-in runtime

registerAgentHarness currently accepts any non-empty id, including pi. In selectAgentHarness, registered harnesses are listed before createPiAgentHarness(), and forced mode returns the first harness whose id matches OPENCLAW_AGENT_RUNTIME; so a plugin that registers id: "pi" can shadow the built-in PI backend. That breaks the explicit PI runtime override/kill-switch behavior and makes backend selection depend on plugin registration order.

Useful? React with 👍 / 👎.

Comment on lines +172 to +176
turnId = turn.turn.id;
projector = new CodexAppServerEventProjector(params, thread.threadId, turnId);
for (const notification of pendingNotifications.splice(0)) {
await handleNotification(notification);
}

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.

P2 Badge Complete attempt when turn/start is already terminal

After turn/start returns, the flow always waits for a later turn/completed notification to resolve completion, but it never checks whether turn.turn.status is already completed/failed/interrupted in the start response. If the app-server returns a terminal turn without emitting a follow-up completion notification (fast/synchronous path), this attempt hangs until timeout despite the turn already being finished.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: qa-channel Channel integration: qa-channel channel: qqbot channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: acpx extensions: anthropic extensions: arcee extensions: byteplus extensions: cloudflare-ai-gateway extensions: copilot-proxy Extension: copilot-proxy extensions: deepseek extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: duckduckgo extensions: fal extensions: huggingface extensions: kilocode extensions: kimi-coding extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: memory-wiki extensions: minimax extensions: moonshot extensions: nvidia extensions: open-prose Extension: open-prose extensions: openai extensions: qa-lab extensions: qianfan extensions: stepfun extensions: synthetic extensions: tavily extensions: together extensions: venice extensions: vercel-ai-gateway extensions: volcengine extensions: webhooks extensions: xiaomi gateway Gateway runtime maintainer Maintainer-authored PR scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.