mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 23:55:12 +00:00
fix: stage ACP and Codex runtime deps
This commit is contained in:
@@ -787,6 +787,18 @@ openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
|
||||
|
||||
Restart the gateway after changing this value.
|
||||
|
||||
### Health probe agent configuration
|
||||
|
||||
The bundled `acpx` plugin probes one harness agent while deciding whether the
|
||||
embedded runtime backend is ready. It defaults to `codex`. If your deployment
|
||||
uses a different default ACP agent, set the probe agent to the same id:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.acpx.config.probeAgent claude
|
||||
```
|
||||
|
||||
Restart the gateway after changing this value.
|
||||
|
||||
## Permission configuration
|
||||
|
||||
ACP sessions run non-interactively — there is no TTY to approve or deny file-write and shell-exec permission prompts. The acpx plugin provides two config keys that control how permissions are handled:
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"probeAgent": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"permissionMode": {
|
||||
"type": "string",
|
||||
"enum": ["approve-all", "approve-reads", "deny-all"]
|
||||
@@ -87,6 +91,11 @@
|
||||
"label": "State Directory",
|
||||
"help": "Directory used for embedded ACP session state and persistence."
|
||||
},
|
||||
"probeAgent": {
|
||||
"label": "Health Probe Agent",
|
||||
"help": "Agent id used for the embedded ACP runtime health probe. Defaults to Codex when unset.",
|
||||
"advanced": true
|
||||
},
|
||||
"permissionMode": {
|
||||
"label": "Permission Mode",
|
||||
"help": "Default permission policy for embedded ACP runtime prompts."
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
],
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
1
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
@@ -25,6 +25,7 @@ declare module "acpx/runtime" {
|
||||
cwd: string;
|
||||
sessionStore: AcpSessionStore;
|
||||
agentRegistry: AcpAgentRegistry;
|
||||
probeAgent?: string;
|
||||
mcpServers?: unknown;
|
||||
permissionMode?: unknown;
|
||||
nonInteractivePermissions?: unknown;
|
||||
|
||||
@@ -26,6 +26,7 @@ export type AcpxMcpServer = {
|
||||
export type AcpxPluginConfig = {
|
||||
cwd?: string;
|
||||
stateDir?: string;
|
||||
probeAgent?: string;
|
||||
permissionMode?: AcpxPermissionMode;
|
||||
nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy;
|
||||
pluginToolsMcpBridge?: boolean;
|
||||
@@ -39,6 +40,7 @@ export type AcpxPluginConfig = {
|
||||
export type ResolvedAcpxPluginConfig = {
|
||||
cwd: string;
|
||||
stateDir: string;
|
||||
probeAgent?: string;
|
||||
permissionMode: AcpxPermissionMode;
|
||||
nonInteractivePermissions: AcpxNonInteractivePermissionPolicy;
|
||||
pluginToolsMcpBridge: boolean;
|
||||
@@ -77,6 +79,7 @@ const McpServerConfigSchema = z.object({
|
||||
export const AcpxPluginConfigSchema = z.strictObject({
|
||||
cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(),
|
||||
stateDir: nonEmptyTrimmedString("stateDir must be a non-empty string").optional(),
|
||||
probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(),
|
||||
permissionMode: z
|
||||
.enum(ACPX_PERMISSION_MODES, {
|
||||
error: `permissionMode must be one of: ${ACPX_PERMISSION_MODES.join(", ")}`,
|
||||
|
||||
@@ -30,6 +30,17 @@ describe("embedded acpx plugin config", () => {
|
||||
expect(resolved.timeoutSeconds).toBe(300);
|
||||
});
|
||||
|
||||
it("keeps explicit probeAgent config", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
probeAgent: "claude",
|
||||
},
|
||||
workspaceDir: "/tmp/openclaw-acpx",
|
||||
});
|
||||
|
||||
expect(resolved.probeAgent).toBe("claude");
|
||||
});
|
||||
|
||||
it("accepts agent command overrides", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
@@ -74,6 +85,7 @@ describe("embedded acpx plugin config", () => {
|
||||
properties: expect.objectContaining({
|
||||
cwd: expect.any(Object),
|
||||
stateDir: expect.any(Object),
|
||||
probeAgent: expect.any(Object),
|
||||
timeoutSeconds: expect.objectContaining({
|
||||
default: 120,
|
||||
}),
|
||||
|
||||
@@ -219,6 +219,7 @@ export function resolveAcpxPluginConfig(params: {
|
||||
return {
|
||||
cwd,
|
||||
stateDir,
|
||||
probeAgent: normalized.probeAgent,
|
||||
permissionMode: normalized.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
||||
nonInteractivePermissions:
|
||||
normalized.nonInteractivePermissions ?? DEFAULT_NON_INTERACTIVE_POLICY,
|
||||
|
||||
22
extensions/acpx/src/manifest.test.ts
Normal file
22
extensions/acpx/src/manifest.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type AcpxPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
bundle?: {
|
||||
stageRuntimeDependencies?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
describe("acpx package manifest", () => {
|
||||
it("opts into staging bundled runtime dependencies", () => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
) as AcpxPackageManifest;
|
||||
|
||||
expect(packageJson.dependencies?.acpx).toBeDefined();
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,7 @@ function createDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntimeLike
|
||||
agentRegistry: createAgentRegistry({
|
||||
overrides: params.pluginConfig.agents,
|
||||
}),
|
||||
probeAgent: params.pluginConfig.probeAgent,
|
||||
mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers),
|
||||
permissionMode: params.pluginConfig.permissionMode,
|
||||
nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions,
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
],
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
extensions/codex/src/manifest.test.ts
Normal file
22
extensions/codex/src/manifest.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type CodexPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
bundle?: {
|
||||
stageRuntimeDependencies?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
describe("codex package manifest", () => {
|
||||
it("opts into staging bundled runtime dependencies", () => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
) as CodexPackageManifest;
|
||||
|
||||
expect(packageJson.dependencies?.["@mariozechner/pi-coding-agent"]).toBeDefined();
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,18 @@ DOCKER_USER="${OPENCLAW_DOCKER_USER:-node}"
|
||||
DOCKER_HOME_MOUNT=()
|
||||
DOCKER_AUTH_PRESTAGED=0
|
||||
|
||||
openclaw_live_acp_bind_append_build_extension() {
|
||||
local extension="${1:?extension required}"
|
||||
local current="${OPENCLAW_DOCKER_BUILD_EXTENSIONS:-${OPENCLAW_EXTENSIONS:-}}"
|
||||
case " $current " in
|
||||
*" $extension "*)
|
||||
;;
|
||||
*)
|
||||
export OPENCLAW_DOCKER_BUILD_EXTENSIONS="${current:+$current }$extension"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
openclaw_live_acp_bind_resolve_auth_provider() {
|
||||
case "${1:-}" in
|
||||
claude) printf '%s\n' "claude-cli" ;;
|
||||
@@ -170,6 +182,7 @@ export OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND="${OPENCLAW_LIVE_ACP_BIND_AGENT_COMM
|
||||
pnpm test:live src/gateway/gateway-acp-bind.live.test.ts
|
||||
EOF
|
||||
|
||||
openclaw_live_acp_bind_append_build_extension acpx
|
||||
"$ROOT_DIR/scripts/test-live-build-docker.sh"
|
||||
|
||||
IFS=',' read -r -a ACP_AGENT_TOKENS <<<"$ACP_AGENT_LIST_RAW"
|
||||
|
||||
@@ -15,6 +15,18 @@ DOCKER_HOME_MOUNT=()
|
||||
DOCKER_EXTRA_ENV_FILES=()
|
||||
DOCKER_AUTH_PRESTAGED=0
|
||||
|
||||
openclaw_live_codex_harness_append_build_extension() {
|
||||
local extension="${1:?extension required}"
|
||||
local current="${OPENCLAW_DOCKER_BUILD_EXTENSIONS:-${OPENCLAW_EXTENSIONS:-}}"
|
||||
case " $current " in
|
||||
*" $extension "*)
|
||||
;;
|
||||
*)
|
||||
export OPENCLAW_DOCKER_BUILD_EXTENSIONS="${current:+$current }$extension"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "$CODEX_HARNESS_AUTH_MODE" in
|
||||
codex-auth | api-key)
|
||||
;;
|
||||
@@ -169,6 +181,7 @@ cd "$tmp_dir"
|
||||
pnpm test:live src/gateway/gateway-codex-harness.live.test.ts
|
||||
EOF
|
||||
|
||||
openclaw_live_codex_harness_append_build_extension codex
|
||||
"$ROOT_DIR/scripts/test-live-build-docker.sh"
|
||||
|
||||
echo "==> Run Codex harness live test in Docker"
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getAcpRuntimeBackend } from "../acp/runtime/registry.js";
|
||||
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
|
||||
import { clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js";
|
||||
import { clearConfigCache, clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import {
|
||||
pinActivePluginChannelRegistry,
|
||||
@@ -213,18 +213,34 @@ async function bindConversationAndWait(params: {
|
||||
originatingAccountId: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<{ mainAssistantTexts: string[]; spawnedSessionKey: string }> {
|
||||
const timeoutMs = params.timeoutMs ?? 90_000;
|
||||
const timeoutMs = params.timeoutMs ?? LIVE_TIMEOUT_MS;
|
||||
const startedAt = Date.now();
|
||||
let attempt = 0;
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
attempt += 1;
|
||||
const backend = getAcpRuntimeBackend("acpx");
|
||||
const runtime = backend?.runtime as { probeAvailability?: () => Promise<void> } | undefined;
|
||||
const runtime = backend?.runtime as
|
||||
| {
|
||||
probeAvailability?: () => Promise<void>;
|
||||
doctor?: () => Promise<{ message?: string; details?: string[] }>;
|
||||
}
|
||||
| undefined;
|
||||
if (runtime?.probeAvailability) {
|
||||
await runtime.probeAvailability().catch(() => {});
|
||||
}
|
||||
if (!(backend?.healthy?.() ?? false)) {
|
||||
if (runtime?.doctor && (attempt === 1 || attempt % 6 === 0)) {
|
||||
const report = await runtime.doctor().catch((error) => ({
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: [],
|
||||
}));
|
||||
logLiveStep(
|
||||
`acpx doctor before bind attempt ${attempt}: ${report.message ?? "unknown"}${
|
||||
report.details?.length ? ` (${report.details.join("; ")})` : ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
logLiveStep(`acpx backend still unhealthy before bind attempt ${attempt}`);
|
||||
await sleep(5_000);
|
||||
continue;
|
||||
@@ -451,6 +467,8 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
},
|
||||
plugins: {
|
||||
...cfg.plugins,
|
||||
enabled: true,
|
||||
allow: Array.from(new Set([...(cfg.plugins?.allow ?? []), "acpx"])),
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
acpx: {
|
||||
@@ -458,6 +476,7 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
enabled: true,
|
||||
config: {
|
||||
...acpxEntry?.config,
|
||||
probeAgent: liveAgent,
|
||||
permissionMode: "approve-all",
|
||||
nonInteractivePermissions: "deny",
|
||||
...(agentCommandOverride
|
||||
@@ -482,12 +501,15 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
};
|
||||
await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`);
|
||||
process.env.OPENCLAW_CONFIG_PATH = tempConfigPath;
|
||||
clearConfigCache();
|
||||
clearRuntimeConfigSnapshot();
|
||||
|
||||
logLiveStep(`starting gateway on port ${String(port)}`);
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token },
|
||||
controlUiEnabled: false,
|
||||
awaitStartupSidecars: true,
|
||||
});
|
||||
logLiveStep("gateway startup returned");
|
||||
await waitForGatewayPort({ host: "127.0.0.1", port, timeoutMs: CONNECT_TIMEOUT_MS });
|
||||
@@ -781,6 +803,7 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
logLiveStep("bound session created cron via MCP and CLI verification passed");
|
||||
} finally {
|
||||
releasePinnedPluginChannelRegistry(channelRegistry);
|
||||
clearConfigCache();
|
||||
clearRuntimeConfigSnapshot();
|
||||
await client.stopAndWait({ timeoutMs: 2_000 }).catch(() => {});
|
||||
await server.close();
|
||||
|
||||
@@ -392,6 +392,7 @@ export async function startGatewayPostAttachRuntime(
|
||||
onPluginServices?: (pluginServices: PluginServicesHandle | null) => void;
|
||||
onSidecarsReady?: () => void;
|
||||
startupTrace?: GatewayStartupTrace;
|
||||
awaitSidecars?: boolean;
|
||||
},
|
||||
runtimeDeps: GatewayPostAttachRuntimeDeps = defaultGatewayPostAttachRuntimeDeps,
|
||||
) {
|
||||
@@ -483,6 +484,19 @@ export async function startGatewayPostAttachRuntime(
|
||||
params.log.warn(`gateway sidecars failed to start: ${String(err)}`);
|
||||
});
|
||||
|
||||
if (params.awaitSidecars === true) {
|
||||
const [stopGatewayUpdateCheck, tailscaleCleanup, sidecarsResult] = await Promise.all([
|
||||
stopGatewayUpdateCheckPromise,
|
||||
tailscaleCleanupPromise,
|
||||
sidecarsPromise,
|
||||
]);
|
||||
return {
|
||||
stopGatewayUpdateCheck,
|
||||
tailscaleCleanup,
|
||||
pluginServices: sidecarsResult.pluginServices,
|
||||
};
|
||||
}
|
||||
|
||||
const [stopGatewayUpdateCheck, tailscaleCleanup] = await Promise.all([
|
||||
stopGatewayUpdateCheckPromise,
|
||||
tailscaleCleanupPromise,
|
||||
|
||||
@@ -227,6 +227,10 @@ export type GatewayServerOptions = {
|
||||
runtime: import("../runtime.js").RuntimeEnv,
|
||||
prompter: import("../wizard/prompts.js").WizardPrompter,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Test-only: wait for post-listen sidecars such as plugin services before returning.
|
||||
*/
|
||||
awaitStartupSidecars?: boolean;
|
||||
/**
|
||||
* Optional startup timestamp used for concise readiness logging.
|
||||
*/
|
||||
@@ -833,6 +837,7 @@ export async function startGatewayServer(
|
||||
startupSidecarsReady = true;
|
||||
},
|
||||
startupTrace,
|
||||
awaitSidecars: opts.awaitStartupSidecars,
|
||||
}),
|
||||
));
|
||||
startupTrace.mark("ready");
|
||||
|
||||
Reference in New Issue
Block a user