Files
openclaw/extensions/codex/src/conversation-turn-input.ts
Vincent Koc ac3cd1a0ca Harden Codex harness control surfaces (#77459)
* fix(scripts): find codex protocol source from worktrees

* fix(test): keep codex harness docker caches writable

* fix(test): relax live codex cache mount permissions

* test(codex): add live docker harness debug output

* fix(test): detect numeric ci env in codex docker harness

* fix(codex): skip duplicate agent-command telemetry

* fix(tooling): skip sparse-missing oxlint tsconfig

* fix(tooling): route changed checks through testbox

* fix(qa): keep coverage json source-clean

* fix(test): preflight codex docker auth

* fix(codex): validate bind option values

* fix(codex): parse quoted command arguments

* fix(codex): reject extra control args

* fix(codex): use content for blank bound prompts

* fix(codex): decode local image file urls

* fix(codex): treat local media urls as images

* fix(codex): keep windows media paths local

* fix(codex): reject malformed diagnostics confirmations

* fix(codex): reject malformed resume commands

* fix(codex): reject malformed thread actions

* fix(codex): reject malformed turn controls

* fix(codex): reject malformed model controls

* fix(codex): resolve empty user input prompts

* fix(codex): enforce user input options

* fix(codex): reject ambiguous computer-use actions

* fix(codex): ignore stale bound turn notifications

* test(gateway): close task registries in gateway harness

* test(gateway): route cleanup through task seams

* fix(codex): describe current permission approvals

* fix(codex): disclose command approval amendments

* fix(codex): preserve approval detail under truncation

* fix(codex): propagate dynamic tool failures

* test(codex): align dynamic tool block contract

* fix(codex): reject extra read-only command operands

* fix(codex): escape command readout fields

* fix(codex): escape status probe errors

* fix(codex): narrow formatted thread details

* fix(codex): escape successful status summaries

* fix(codex): escape bound control replies

* fix(codex): escape user input prompts

* fix(codex): escape control failure replies

* fix(codex): escape approval prompt text

* test(codex): narrow escaped reply assertions

* test(codex): complete strict reply fixtures

* test(codex): preserve account fixture literals

* test(codex): align status probe fixtures

* fix(codex): satisfy sanitizer regex lint

* fix(codex): harden command readouts

* fix(codex): harden bound image inputs

* fix(codex): sanitize command failure replies

* test(codex): complete rate limit fixture

* test(tooling): isolate postinstall compile cache fixture

* fix(codex): keep app-server event ownership explicit

---------

Co-authored-by: pashpashpash <nik@vault77.ai>
2026-05-05 07:23:41 +09:00

107 lines
3.3 KiB
TypeScript

import path from "node:path";
import { fileURLToPath } from "node:url";
import type { PluginHookInboundClaimEvent } from "openclaw/plugin-sdk/plugin-entry";
import type { CodexUserInput } from "./app-server/protocol.js";
type InboundMedia = {
path?: string;
url?: string;
mimeType?: string;
};
const IMAGE_EXTENSIONS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".webp"]);
export function buildCodexConversationTurnInput(params: {
prompt: string;
event: PluginHookInboundClaimEvent;
}): CodexUserInput[] {
return [
{ type: "text", text: params.prompt, text_elements: [] },
...extractInboundMedia(params.event)
.map(toCodexImageInput)
.filter((item): item is CodexUserInput => item !== undefined),
];
}
function extractInboundMedia(event: PluginHookInboundClaimEvent): InboundMedia[] {
const metadata = event.metadata ?? {};
// OpenClaw channels expose either local staged files or remote URLs. Keep
// them separate so Codex can receive the cheaper localImage input when a file
// is already present, while still supporting remote-only transports.
const paths = readStringArray(metadata.mediaPaths).concat(readStringArray(metadata.mediaPath));
const urls = readStringArray(metadata.mediaUrls).concat(readStringArray(metadata.mediaUrl));
const mimeTypes = readStringArray(metadata.mediaTypes).concat(
readStringArray(metadata.mediaType),
);
const count = Math.max(paths.length, urls.length, mimeTypes.length);
const media: InboundMedia[] = [];
for (let index = 0; index < count; index += 1) {
media.push({
path: paths[index],
url: urls[index],
mimeType: mimeTypes[index] ?? mimeTypes[0],
});
}
return media;
}
function toCodexImageInput(media: InboundMedia): CodexUserInput | undefined {
if (!isImageMedia(media)) {
return undefined;
}
const localPath = media.path ?? readLocalMediaPath(media.url);
if (localPath) {
const normalized = normalizeFileUrl(localPath);
return normalized ? { type: "localImage", path: normalized } : undefined;
}
return media.url ? { type: "image", url: media.url } : undefined;
}
function isImageMedia(media: InboundMedia): boolean {
if (media.mimeType?.toLowerCase().startsWith("image/")) {
return true;
}
const candidate = media.path ?? media.url;
if (!candidate) {
return false;
}
return IMAGE_EXTENSIONS.has(path.extname(candidate.split(/[?#]/, 1)[0] ?? "").toLowerCase());
}
function normalizeFileUrl(value: string): string | undefined {
if (!value.startsWith("file://")) {
return value;
}
try {
return fileURLToPath(value);
} catch {
return undefined;
}
}
function readLocalMediaPath(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
if (value.startsWith("file://")) {
return value;
}
if (value.startsWith("//")) {
return undefined;
}
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) {
return value;
}
return /^[a-z][a-z0-9+.-]*:/i.test(value) ? undefined : value;
}
function readStringArray(value: unknown): string[] {
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
if (!Array.isArray(value)) {
return [];
}
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
}