Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
770
extensions/acpx/src/codex-auth-bridge.ts
Normal file
770
extensions/acpx/src/codex-auth-bridge.ts
Normal file
@@ -0,0 +1,770 @@
|
||||
/**
|
||||
* Prepares isolated Codex and Claude ACP wrapper commands for ACPX. The bridge
|
||||
* copies safe auth/config state into plugin-owned homes and redacts diagnostics.
|
||||
*/
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
extractTrustedCodexProjectPaths,
|
||||
renderIsolatedCodexConfig,
|
||||
} from "./codex-trust-config.js";
|
||||
import { quoteCommandPart, splitCommandParts } from "./command-line.js";
|
||||
import { resolveAcpxPluginRoot } from "./config.js";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
import {
|
||||
OPENCLAW_ACPX_LEASE_ID_ARG,
|
||||
OPENCLAW_ACPX_LEASE_ID_ENV,
|
||||
OPENCLAW_GATEWAY_INSTANCE_ID_ARG,
|
||||
} from "./process-lease.js";
|
||||
|
||||
const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
|
||||
const CODEX_ACP_BIN = "codex-acp";
|
||||
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
|
||||
const CLAUDE_ACP_BIN = "claude-agent-acp";
|
||||
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
|
||||
type PackageManifest = {
|
||||
name?: unknown;
|
||||
bin?: unknown;
|
||||
dependencies?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function readSelfManifest(): PackageManifest {
|
||||
const manifestPath = path.join(resolveAcpxPluginRoot(import.meta.url), "package.json");
|
||||
return JSON.parse(fsSync.readFileSync(manifestPath, "utf8")) as PackageManifest;
|
||||
}
|
||||
|
||||
function readManifestDependencyVersion(packageName: string): string {
|
||||
const version = readSelfManifest().dependencies?.[packageName];
|
||||
if (typeof version !== "string" || version.trim() === "") {
|
||||
throw new Error(`Missing ${packageName} dependency version in @openclaw/acpx manifest`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
|
||||
const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
|
||||
|
||||
function basename(value: string): string {
|
||||
return value.split(/[\\/]/).pop() ?? value;
|
||||
}
|
||||
|
||||
function resolvePackageBinPath(
|
||||
packageJsonPath: string,
|
||||
manifest: PackageManifest,
|
||||
binName: string,
|
||||
): string | undefined {
|
||||
const { bin } = manifest;
|
||||
const relativeBinPath =
|
||||
typeof bin === "string"
|
||||
? bin
|
||||
: bin && typeof bin === "object"
|
||||
? (bin as Record<string, unknown>)[binName]
|
||||
: undefined;
|
||||
if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") {
|
||||
return undefined;
|
||||
}
|
||||
return path.resolve(path.dirname(packageJsonPath), relativeBinPath);
|
||||
}
|
||||
|
||||
async function resolveInstalledAcpPackageBinPath(
|
||||
packageName: string,
|
||||
binName: string,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
|
||||
const { value: manifest } = await readJsonFileWithFallback<PackageManifest>(
|
||||
packageJsonPath,
|
||||
{},
|
||||
);
|
||||
if (manifest.name !== packageName) {
|
||||
return undefined;
|
||||
}
|
||||
const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName);
|
||||
if (!binPath) {
|
||||
return undefined;
|
||||
}
|
||||
await fs.access(binPath);
|
||||
return binPath;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveInstalledCodexAcpBinPath(): Promise<string | undefined> {
|
||||
// Keep OpenClaw's isolated CODEX_HOME wrapper, but launch the plugin-local
|
||||
// Codex ACP adapter when the package dependency is available.
|
||||
return await resolveInstalledAcpPackageBinPath(CODEX_ACP_PACKAGE, CODEX_ACP_BIN);
|
||||
}
|
||||
|
||||
async function resolveInstalledClaudeAcpBinPath(): Promise<string | undefined> {
|
||||
return await resolveInstalledAcpPackageBinPath(CLAUDE_ACP_PACKAGE, CLAUDE_ACP_BIN);
|
||||
}
|
||||
|
||||
type DiagnosticRedactionRuleSpec = {
|
||||
source: string;
|
||||
flags: string;
|
||||
replacement: string;
|
||||
};
|
||||
|
||||
const DIAGNOSTIC_REDACTION_RULES: DiagnosticRedactionRuleSpec[] = [
|
||||
{
|
||||
source: String.raw`(authorization\s*[:=]\s*bearer\s+)[^\s'"<>]+`,
|
||||
flags: "gi",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source: String.raw`((?:api[_-]?key|apiKey|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|password|passwd|credential)\s*[:=]\s*)[^\s'"<>]+`,
|
||||
flags: "gi",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source: String.raw`("(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*")[^"]+`,
|
||||
flags: "g",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source: String.raw`(["']?(?:api[-_]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|idToken|auth[-_]?token|authToken|client[-_]?secret|clientSecret|app[-_]?secret|appSecret|token|secret|password|passwd|credential)["']?\s*[:=]\s*["']?)[^"',}\s<>]+`,
|
||||
flags: "gi",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source: String.raw`([?&](?:access[-_]?token|auth[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature)=)[^&\s'"<>]+`,
|
||||
flags: "gi",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source: String.raw`(--(?:api[-_]?key|token|secret|password|passwd)\s+)[^\s'"]+`,
|
||||
flags: "gi",
|
||||
replacement: "$1[REDACTED]",
|
||||
},
|
||||
{
|
||||
source:
|
||||
String.raw`-----BEGIN [A-Z ]*PRI` +
|
||||
String.raw`VATE KEY-----[\s\S]+?-----END [A-Z ]*PRI` +
|
||||
String.raw`VATE KEY-----`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_PRIVATE_KEY]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_OPENAI_KEY]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(gh[pousr]_[A-Za-z0-9_]{20,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_GITHUB_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_GITHUB_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_SLACK_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(gsk_[A-Za-z0-9_-]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_API_KEY]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_GOOGLE_KEY]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(ya29\.[0-9A-Za-z_\-./+=]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_GOOGLE_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_JWT]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_API_KEY]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(npm_[A-Za-z0-9]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_NPM_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(LTAI[A-Za-z0-9]{10,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_ACCESS_KEY]",
|
||||
},
|
||||
{ source: String.raw`\b(hf_[A-Za-z0-9]{10,})\b`, flags: "g", replacement: "[REDACTED_API_KEY]" },
|
||||
{
|
||||
source: String.raw`\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
||||
flags: "g",
|
||||
replacement: "bot[REDACTED_TELEGRAM_TOKEN]",
|
||||
},
|
||||
{
|
||||
source: String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
||||
flags: "g",
|
||||
replacement: "[REDACTED_TELEGRAM_TOKEN]",
|
||||
},
|
||||
];
|
||||
|
||||
function renderDiagnosticRedactionRuleSpecs(): string {
|
||||
return JSON.stringify(DIAGNOSTIC_REDACTION_RULES);
|
||||
}
|
||||
|
||||
function buildAdapterWrapperScript(params: {
|
||||
displayName: string;
|
||||
packageSpec: string;
|
||||
binName: string;
|
||||
installedBinPath?: string;
|
||||
envSetup: string;
|
||||
stderrLogFileNamePrefix?: string;
|
||||
}): string {
|
||||
return `#!/usr/bin/env node
|
||||
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
${params.envSetup}
|
||||
const stderrLogFileNamePrefix = ${params.stderrLogFileNamePrefix ? JSON.stringify(params.stderrLogFileNamePrefix) : "undefined"};
|
||||
const stderrLogMaxChars = 256 * 1024;
|
||||
|
||||
const openClawWrapperArgs = new Set([
|
||||
${quoteCommandPart(OPENCLAW_ACPX_LEASE_ID_ARG)},
|
||||
${quoteCommandPart(OPENCLAW_GATEWAY_INSTANCE_ID_ARG)},
|
||||
]);
|
||||
|
||||
function readOpenClawWrapperArg(args, name) {
|
||||
const index = args.indexOf(name);
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const value = args[index + 1];
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function safeDiagnosticFilePart(value) {
|
||||
const sanitized = String(value || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120);
|
||||
return sanitized || "pid-" + process.pid;
|
||||
}
|
||||
|
||||
function resolveStderrLogPath(args) {
|
||||
if (!stderrLogFileNamePrefix) {
|
||||
return undefined;
|
||||
}
|
||||
const leaseId =
|
||||
process.env[${JSON.stringify(OPENCLAW_ACPX_LEASE_ID_ENV)}] ||
|
||||
readOpenClawWrapperArg(args, ${quoteCommandPart(OPENCLAW_ACPX_LEASE_ID_ARG)}) ||
|
||||
"pid-" + process.pid;
|
||||
const fileName = stderrLogFileNamePrefix + "." + safeDiagnosticFilePart(leaseId) + ".log";
|
||||
return fileURLToPath(new URL("./" + fileName, import.meta.url));
|
||||
}
|
||||
|
||||
const diagnosticRedactionRules = ${renderDiagnosticRedactionRuleSpecs()}.map((rule) => [
|
||||
new RegExp(rule.source, rule.flags),
|
||||
rule.replacement,
|
||||
]);
|
||||
|
||||
function redactDiagnosticText(text) {
|
||||
let redacted = text;
|
||||
for (const [pattern, replacement] of diagnosticRedactionRules) {
|
||||
redacted = redacted.replace(pattern, replacement);
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
let pendingStderrLogText = "";
|
||||
const stderrPrivateKeyEndPattern = /-----END [A-Z ]*PRIVATE KEY-----/;
|
||||
|
||||
function hasUnclosedPrivateKeyBlock(text) {
|
||||
let lastBeginIndex = -1;
|
||||
for (const match of text.matchAll(/-----BEGIN [A-Z ]*PRIVATE KEY-----/g)) {
|
||||
lastBeginIndex = match.index ?? lastBeginIndex;
|
||||
}
|
||||
if (lastBeginIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
return stderrPrivateKeyEndPattern.test(text.slice(lastBeginIndex)) ? -1 : lastBeginIndex;
|
||||
}
|
||||
|
||||
function writeRedactedStderrLog(text) {
|
||||
if (!stderrLogPath) {
|
||||
return;
|
||||
}
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
appendFileSync(stderrLogPath, redactDiagnosticText(text), "utf8");
|
||||
const current = readFileSync(stderrLogPath, "utf8");
|
||||
if (current.length > stderrLogMaxChars) {
|
||||
writeFileSync(stderrLogPath, current.slice(-stderrLogMaxChars), "utf8");
|
||||
}
|
||||
} catch {
|
||||
// Stderr capture is diagnostic-only; never break the ACP adapter.
|
||||
}
|
||||
}
|
||||
|
||||
function redactIncompletePrivateKeyTail(text) {
|
||||
const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(text);
|
||||
if (unclosedPrivateKeyStart === -1) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, unclosedPrivateKeyStart) + "[REDACTED_PRIVATE_KEY]";
|
||||
}
|
||||
|
||||
function flushFinalizedStderrLogText() {
|
||||
const lastLineBreak = pendingStderrLogText.lastIndexOf("\\n");
|
||||
if (lastLineBreak === -1) {
|
||||
if (pendingStderrLogText.length > stderrLogMaxChars) {
|
||||
pendingStderrLogText = pendingStderrLogText.slice(-stderrLogMaxChars);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let flushEnd = lastLineBreak + 1;
|
||||
const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(
|
||||
pendingStderrLogText.slice(0, flushEnd),
|
||||
);
|
||||
if (unclosedPrivateKeyStart !== -1) {
|
||||
flushEnd = unclosedPrivateKeyStart;
|
||||
}
|
||||
if (flushEnd <= 0) {
|
||||
if (pendingStderrLogText.length > stderrLogMaxChars) {
|
||||
pendingStderrLogText = pendingStderrLogText.slice(-stderrLogMaxChars);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const finalizedText = pendingStderrLogText.slice(0, flushEnd);
|
||||
pendingStderrLogText = pendingStderrLogText.slice(flushEnd);
|
||||
writeRedactedStderrLog(finalizedText);
|
||||
}
|
||||
|
||||
function appendStderrLog(chunk) {
|
||||
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
pendingStderrLogText += text;
|
||||
flushFinalizedStderrLogText();
|
||||
}
|
||||
|
||||
function finishStderrLog() {
|
||||
const text = redactIncompletePrivateKeyTail(pendingStderrLogText);
|
||||
pendingStderrLogText = "";
|
||||
writeRedactedStderrLog(text);
|
||||
}
|
||||
|
||||
function stripOpenClawWrapperArgs(args) {
|
||||
const stripped = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const value = args[index];
|
||||
if (openClawWrapperArgs.has(value)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
stripped.push(value);
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
const rawConfiguredArgs = process.argv.slice(2);
|
||||
const stderrLogPath = resolveStderrLogPath(rawConfiguredArgs);
|
||||
|
||||
try {
|
||||
if (stderrLogPath) {
|
||||
writeFileSync(stderrLogPath, "", "utf8");
|
||||
}
|
||||
} catch {
|
||||
// Stderr capture is diagnostic-only; never break the ACP adapter.
|
||||
}
|
||||
|
||||
const configuredArgs = stripOpenClawWrapperArgs(rawConfiguredArgs);
|
||||
|
||||
function resolveNpmCliPath() {
|
||||
const candidate = path.resolve(
|
||||
path.dirname(process.execPath),
|
||||
"..",
|
||||
"lib",
|
||||
"node_modules",
|
||||
"npm",
|
||||
"bin",
|
||||
"npm-cli.js",
|
||||
);
|
||||
return existsSync(candidate) ? candidate : undefined;
|
||||
}
|
||||
|
||||
const npmCliPath = resolveNpmCliPath();
|
||||
const installedBinPath = ${params.installedBinPath ? quoteCommandPart(params.installedBinPath) : "undefined"};
|
||||
let defaultCommand;
|
||||
let defaultArgs;
|
||||
if (installedBinPath) {
|
||||
defaultCommand = process.execPath;
|
||||
defaultArgs = [installedBinPath];
|
||||
} else if (npmCliPath) {
|
||||
defaultCommand = process.execPath;
|
||||
defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
|
||||
} else {
|
||||
defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||
defaultArgs = ["--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
|
||||
}
|
||||
const command =
|
||||
configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" ? configuredArgs[1] : defaultCommand;
|
||||
const args =
|
||||
configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}"
|
||||
? configuredArgs.slice(2)
|
||||
: [...defaultArgs, ...configuredArgs];
|
||||
|
||||
if (!command) {
|
||||
console.error("[openclaw] missing configured ${params.displayName} ACP command");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
detached: process.platform !== "win32",
|
||||
env,
|
||||
stdio: ["inherit", "inherit", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
appendStderrLog(chunk);
|
||||
process.stderr.write(chunk);
|
||||
});
|
||||
|
||||
let forceKillTimer;
|
||||
let orphanCleanupStarted = false;
|
||||
let childExitCode = 1;
|
||||
|
||||
function killChildTree(signal, options = {}) {
|
||||
if (!child.pid || (!options.force && child.killed)) {
|
||||
return;
|
||||
}
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
// The adapter can spawn grandchildren; signaling the process group keeps
|
||||
// the generated wrapper from leaving an ACP tree behind.
|
||||
process.kill(-child.pid, signal);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back to direct child signaling below.
|
||||
}
|
||||
}
|
||||
child.kill(signal);
|
||||
}
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
||||
process.once(signal, () => {
|
||||
killChildTree(signal);
|
||||
});
|
||||
}
|
||||
|
||||
const originalParentPid = process.ppid;
|
||||
const parentWatcher =
|
||||
process.platform === "win32"
|
||||
? undefined
|
||||
: setInterval(() => {
|
||||
// Orphan detection: parent PID changed means our original parent died.
|
||||
// The new parent could be PID 1 (init) on bare-metal hosts, OR a
|
||||
// systemd user-session manager, OR a container init, OR a session
|
||||
// leader — depending on environment. Previously this only triggered
|
||||
// on PPID == 1, which missed all systemd-managed deployments and
|
||||
// leaked codex-acp adapter trees on every gateway restart.
|
||||
if (process.ppid === originalParentPid) {
|
||||
return;
|
||||
}
|
||||
if (orphanCleanupStarted) {
|
||||
return;
|
||||
}
|
||||
orphanCleanupStarted = true;
|
||||
if (parentWatcher) {
|
||||
clearInterval(parentWatcher);
|
||||
}
|
||||
killChildTree("SIGTERM");
|
||||
// Keep the wrapper alive long enough for stubborn adapters to receive
|
||||
// a forced fallback signal after SIGTERM.
|
||||
forceKillTimer = setTimeout(() => {
|
||||
killChildTree("SIGKILL", { force: true });
|
||||
childExitCode = 1;
|
||||
}, 1_500);
|
||||
}, 1_000);
|
||||
parentWatcher?.unref?.();
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (parentWatcher) {
|
||||
clearInterval(parentWatcher);
|
||||
}
|
||||
if (orphanCleanupStarted) {
|
||||
return;
|
||||
}
|
||||
if (forceKillTimer) {
|
||||
clearTimeout(forceKillTimer);
|
||||
}
|
||||
if (code !== null) {
|
||||
childExitCode = code;
|
||||
return;
|
||||
}
|
||||
childExitCode = signal ? 1 : 0;
|
||||
});
|
||||
|
||||
child.on("close", () => {
|
||||
finishStderrLog();
|
||||
process.exit(childExitCode);
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
|
||||
return buildAdapterWrapperScript({
|
||||
displayName: "Codex",
|
||||
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
|
||||
binName: CODEX_ACP_BIN,
|
||||
installedBinPath,
|
||||
stderrLogFileNamePrefix: "codex-acp-wrapper.stderr",
|
||||
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
|
||||
const codexAuthPath = fileURLToPath(new URL("./codex-home/auth.json", import.meta.url));
|
||||
const codexApiKey = (process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || "").trim();
|
||||
let shouldWriteCodexApiKeyAuth = false;
|
||||
if (codexApiKey) {
|
||||
if (!existsSync(codexAuthPath)) {
|
||||
shouldWriteCodexApiKeyAuth = true;
|
||||
} else {
|
||||
try {
|
||||
const existingCodexAuth = JSON.parse(readFileSync(codexAuthPath, "utf8"));
|
||||
shouldWriteCodexApiKeyAuth =
|
||||
!existingCodexAuth ||
|
||||
typeof existingCodexAuth !== "object" ||
|
||||
typeof existingCodexAuth.OPENAI_API_KEY === "string";
|
||||
} catch {
|
||||
shouldWriteCodexApiKeyAuth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldWriteCodexApiKeyAuth) {
|
||||
writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
OPENAI_API_KEY: codexApiKey,
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
}) + "\\n",
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
const env = {
|
||||
...process.env,
|
||||
CODEX_HOME: codexHome,
|
||||
};`,
|
||||
});
|
||||
}
|
||||
|
||||
function buildClaudeAcpWrapperScript(installedBinPath?: string): string {
|
||||
return buildAdapterWrapperScript({
|
||||
displayName: "Claude",
|
||||
// This package is patched in OpenClaw; fallback must not float to an unpatched newer release.
|
||||
packageSpec: `${CLAUDE_ACP_PACKAGE}@${CLAUDE_ACP_PACKAGE_VERSION}`,
|
||||
binName: CLAUDE_ACP_BIN,
|
||||
installedBinPath,
|
||||
envSetup: `const env = {
|
||||
...process.env,
|
||||
};`,
|
||||
});
|
||||
}
|
||||
|
||||
async function readSourceCodexConfig(codexHome: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await fs.readFile(path.join(codexHome, "config.toml"), "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareIsolatedCodexHome(params: {
|
||||
baseDir: string;
|
||||
workspaceDir: string;
|
||||
}): Promise<string> {
|
||||
const sourceCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
||||
const sourceConfig = await readSourceCodexConfig(sourceCodexHome);
|
||||
const trustedProjectPaths = [
|
||||
...(sourceConfig ? extractTrustedCodexProjectPaths(sourceConfig) : []),
|
||||
params.workspaceDir,
|
||||
];
|
||||
const codexHome = path.join(params.baseDir, "codex-home");
|
||||
await fs.mkdir(codexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(codexHome, "config.toml"),
|
||||
renderIsolatedCodexConfig({
|
||||
sourceConfigToml: sourceConfig,
|
||||
projectPaths: trustedProjectPaths,
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
return codexHome;
|
||||
}
|
||||
|
||||
async function makeGeneratedWrapperExecutableIfPossible(wrapperPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.chmod(wrapperPath, 0o755);
|
||||
} catch {
|
||||
// The wrapper is invoked via `node wrapper.mjs`; executable mode is only a convenience.
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
const wrapperPath = path.join(baseDir, "codex-acp-wrapper.mjs");
|
||||
await fs.writeFile(wrapperPath, buildCodexAcpWrapperScript(installedBinPath), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
async function writeClaudeAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
const wrapperPath = path.join(baseDir, "claude-agent-acp-wrapper.mjs");
|
||||
await fs.writeFile(wrapperPath, buildClaudeAcpWrapperScript(installedBinPath), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
function buildWrapperCommand(wrapperPath: string, args: string[] = []): string {
|
||||
return [process.execPath, wrapperPath, ...args].map(quoteCommandPart).join(" ");
|
||||
}
|
||||
|
||||
function isAcpPackageSpec(value: string, packageName: string): boolean {
|
||||
const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`^${escapedPackageName}(?:@.+)?$`, "i").test(value.trim());
|
||||
}
|
||||
|
||||
function isAcpBinName(value: string, binName: string): boolean {
|
||||
const commandName = basename(value);
|
||||
const escapedBinName = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`^${escapedBinName}(?:\\.exe|\\.[cm]?js)?$`, "i").test(commandName);
|
||||
}
|
||||
|
||||
function isPackageRunnerCommand(value: string): boolean {
|
||||
return /^(?:npx|npm|pnpm|bunx)(?:\.cmd|\.exe)?$/i.test(basename(value));
|
||||
}
|
||||
|
||||
function extractConfiguredAdapterArgs(params: {
|
||||
configuredCommand?: string;
|
||||
packageName: string;
|
||||
binName: string;
|
||||
}): string[] | undefined {
|
||||
const trimmedConfiguredCommand = params.configuredCommand?.trim();
|
||||
if (!trimmedConfiguredCommand) {
|
||||
return [];
|
||||
}
|
||||
const parts = splitCommandParts(trimmedConfiguredCommand);
|
||||
if (!parts.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packageIndex = parts.findIndex((part) => isAcpPackageSpec(part, params.packageName));
|
||||
if (packageIndex >= 0) {
|
||||
if (!isPackageRunnerCommand(parts[0] ?? "")) {
|
||||
return undefined;
|
||||
}
|
||||
const afterPackage = parts.slice(packageIndex + 1);
|
||||
if (afterPackage[0] === "--" && isAcpBinName(afterPackage[1] ?? "", params.binName)) {
|
||||
return afterPackage.slice(2);
|
||||
}
|
||||
if (isAcpBinName(afterPackage[0] ?? "", params.binName)) {
|
||||
return afterPackage.slice(1);
|
||||
}
|
||||
return afterPackage[0] === "--" ? afterPackage.slice(1) : afterPackage;
|
||||
}
|
||||
|
||||
if (isAcpBinName(parts[0] ?? "", params.binName)) {
|
||||
return parts.slice(1);
|
||||
}
|
||||
if (basename(parts[0] ?? "") === "node" && isAcpBinName(parts[1] ?? "", params.binName)) {
|
||||
return parts.slice(2);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildCodexAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
|
||||
const configuredAdapterArgs = extractConfiguredAdapterArgs({
|
||||
configuredCommand,
|
||||
packageName: CODEX_ACP_PACKAGE,
|
||||
binName: CODEX_ACP_BIN,
|
||||
});
|
||||
if (configuredAdapterArgs) {
|
||||
return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
|
||||
}
|
||||
return buildWrapperCommand(wrapperPath, [
|
||||
RUN_CONFIGURED_COMMAND_SENTINEL,
|
||||
...splitCommandParts(configuredCommand?.trim() ?? ""),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildClaudeAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
|
||||
const configuredAdapterArgs = extractConfiguredAdapterArgs({
|
||||
configuredCommand,
|
||||
packageName: CLAUDE_ACP_PACKAGE,
|
||||
binName: CLAUDE_ACP_BIN,
|
||||
});
|
||||
if (configuredAdapterArgs) {
|
||||
return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
|
||||
}
|
||||
return configuredCommand?.trim() || buildWrapperCommand(wrapperPath);
|
||||
}
|
||||
|
||||
/** Prepare ACPX agent commands and isolated auth homes for Codex/Claude adapters. */
|
||||
export async function prepareAcpxCodexAuthConfig(params: {
|
||||
pluginConfig: ResolvedAcpxPluginConfig;
|
||||
stateDir: string;
|
||||
logger?: unknown;
|
||||
resolveInstalledCodexAcpBinPath?: () => Promise<string | undefined>;
|
||||
resolveInstalledClaudeAcpBinPath?: () => Promise<string | undefined>;
|
||||
}): Promise<ResolvedAcpxPluginConfig> {
|
||||
void params.logger;
|
||||
const codexBaseDir = path.join(params.stateDir, "acpx");
|
||||
await prepareIsolatedCodexHome({
|
||||
baseDir: codexBaseDir,
|
||||
workspaceDir: params.pluginConfig.cwd,
|
||||
});
|
||||
const installedCodexBinPath = await (
|
||||
params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath
|
||||
)();
|
||||
const installedClaudeBinPath = await (
|
||||
params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath
|
||||
)();
|
||||
const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedCodexBinPath);
|
||||
const claudeWrapperPath = await writeClaudeAcpWrapper(codexBaseDir, installedClaudeBinPath);
|
||||
const configuredCodexCommand = params.pluginConfig.agents.codex;
|
||||
const configuredClaudeCommand = params.pluginConfig.agents.claude;
|
||||
|
||||
return {
|
||||
...params.pluginConfig,
|
||||
agents: {
|
||||
...params.pluginConfig.agents,
|
||||
codex: buildCodexAcpWrapperCommand(wrapperPath, configuredCodexCommand),
|
||||
claude: buildClaudeAcpWrapperCommand(claudeWrapperPath, configuredClaudeCommand),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user