Files
openclaw/src/gateway/http-utils.ts
T
Peter Steinberger f94a7dc183 feat(codex): supervise native Codex sessions (#104045)
* feat(codex): add native session supervision

* fix(codex): harden supervision integration

* fix(codex): preserve locked harness ownership

* fix(codex): fence native session archive

* fix(codex): revalidate archive binding ownership

* feat(codex): integrate supervision runtime

* feat(sessions): preserve harness-owned execution

* feat(sessions): persist harness ownership invariants

* feat(gateway): enforce harness-owned sessions

* feat(setup): enable detected Codex supervision

* feat(mac): expose supervised Codex sessions

* feat(ui): make Codex sessions actionable

* docs(codex): document session supervision

* test(codex): cover integration ownership

* chore(i18n): refresh supervision inventories

* fix(setup): finalize Codex activation atomically

* test(codex): narrow binding store update

* fix(sessions): preserve legacy model locks

* test(macos): serialize Codex catalog fixtures

* fix(sessions): preserve legacy lock admission

* chore(i18n): reconcile supervision metadata

* test(sessions): mark legacy lock fixture

* fix(macos): drain final Codex catalog frame

* docs: leave supervision note to release

* style(macos): satisfy Codex catalog type length

* chore: record session accessor seam owners

* fix(macos): honor configured Codex supervision

* fix(codex): preserve harness-owned model locks

* fix(codex): satisfy supervision lint gates

* chore(i18n): refresh native supervision inventory

* fix(codex): align supervision validation contracts

* fix(codex): close supervision boundary gaps

* fix(codex): preserve supervision activation contracts

* fix(codex): dispose standalone supervision runtime

* fix(codex): pin supervised source connection

* fix(plugins): bind delegated runs to exact session target

* fix(codex): scope supervised sessions to configured agents

* fix(codex): fingerprint effective supervision home

* fix(codex): normalize supervision plugin policy

* fix(codex): keep supervised bindings stable across upgrades

* fix(codex): guard all supervised binding connections

* fix(codex): preserve catalog filters and pending CAS identity

* fix(codex): preserve supervision identity for diagnostics

* fix(codex): bind uncertain commits to supervision connection

* fix(codex): satisfy supervision type boundaries

* fix(macos): reconcile current main validation

* fix(codex): handle absent runtime config in supervision

* fix(doctor): own local audio acceleration check

* fix(codex): satisfy integration lint gates

* fix(codex): satisfy lifecycle safety guards
2026-07-11 00:12:08 -07:00

270 lines
8.8 KiB
TypeScript

// Gateway HTTP request helpers.
// Resolves OpenAI-compatible agent/model/session headers and re-exports auth helpers.
import { randomUUID } from "node:crypto";
import type { IncomingMessage } from "node:http";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { modelKey, parseModelRef, resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { createModelVisibilityPolicy } from "../agents/model-visibility-policy.js";
import { getRuntimeConfig } from "../config/io.js";
import { resolveSessionEntryAccessTarget } from "../config/sessions/session-accessor.js";
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
import {
buildAgentMainSessionKey,
isAcpSessionKey,
isCronSessionKey,
isSubagentSessionKey,
isValidAgentId,
normalizeAgentId,
} from "../routing/session-key.js";
import {
isAgentHarnessSessionKey,
isAgentHarnessSessionStoreEntryProtected,
} from "../sessions/agent-harness-session-key.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { getHeader } from "./http-auth-utils.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
import { canonicalizeSessionKeyForAgent } from "./session-store-key.js";
export {
authorizeOpenAiCompatibleHttpModelOverride,
authorizeGatewayHttpRequestOrReply,
authorizeScopedGatewayHttpRequestOrReply,
checkGatewayHttpRequestAuth,
getBearerToken,
getHeader,
isGatewayBearerHttpRequest,
resolveHttpBrowserOriginPolicy,
resolveHttpSenderIsOwner,
resolveOpenAiCompatibleHttpOperatorScopes,
resolveOpenAiCompatibleHttpSenderIsOwner,
resolveSharedSecretHttpOperatorScopes,
resolveTrustedHttpOperatorScopes,
type AuthorizedGatewayHttpRequest,
type GatewayHttpRequestAuthCheckResult,
} from "./http-auth-utils.js";
export const OPENCLAW_MODEL_ID = "openclaw";
/** Default OpenAI-compatible model alias that targets the default OpenClaw agent. */
export const OPENCLAW_DEFAULT_MODEL_ID = "openclaw/default";
class UnknownGatewayAgentError extends Error {
constructor(readonly agentId: string) {
super(`Unknown agent '${agentId}'.`);
this.name = "UnknownGatewayAgentError";
}
}
export class GatewaySessionKeyOverrideError extends Error {
constructor() {
super("`x-openclaw-session-key` cannot use reserved internal session namespaces.");
this.name = "GatewaySessionKeyOverrideError";
}
}
export function isUnknownGatewayAgentError(err: unknown): err is UnknownGatewayAgentError {
return err instanceof UnknownGatewayAgentError;
}
export function isGatewaySessionKeyOverrideError(
err: unknown,
): err is GatewaySessionKeyOverrideError {
return err instanceof GatewaySessionKeyOverrideError;
}
function assertKnownAgentId(agentId: string, cfg = getRuntimeConfig()): void {
if (!listAgentIds(cfg).includes(agentId)) {
throw new UnknownGatewayAgentError(agentId);
}
}
function resolveAgentIdFromHeader(req: IncomingMessage): string | undefined {
const raw =
normalizeOptionalString(getHeader(req, "x-openclaw-agent-id")) ||
normalizeOptionalString(getHeader(req, "x-openclaw-agent")) ||
"";
if (!raw) {
return undefined;
}
if (!isValidAgentId(raw)) {
throw new UnknownGatewayAgentError(raw);
}
return normalizeAgentId(raw);
}
/** Resolves the target agent encoded by an OpenAI-compatible model id. */
export function resolveAgentIdFromModel(
model: string | undefined,
cfg = getRuntimeConfig(),
): string | undefined {
const raw = model?.trim();
if (!raw) {
return undefined;
}
const lowered = normalizeLowercaseStringOrEmpty(raw);
if (lowered === OPENCLAW_MODEL_ID || lowered === OPENCLAW_DEFAULT_MODEL_ID) {
return resolveDefaultAgentId(cfg);
}
const m =
raw.match(/^openclaw[:/](?<agentId>[a-z0-9][a-z0-9_-]{0,63})$/i) ??
raw.match(/^agent:(?<agentId>[a-z0-9][a-z0-9_-]{0,63})$/i);
const agentId = m?.groups?.agentId;
if (!agentId) {
return undefined;
}
return normalizeAgentId(agentId);
}
/** Validates and resolves the `x-openclaw-model` override for OpenAI-compatible requests. */
export async function resolveOpenAiCompatModelOverride(params: {
req: IncomingMessage;
agentId: string;
model: string | undefined;
}): Promise<{ modelOverride?: string; errorMessage?: string }> {
const requestModel = params.model?.trim();
if (requestModel && !resolveAgentIdFromModel(requestModel)) {
return {
errorMessage: "Invalid `model`. Use `openclaw` or `openclaw/<agentId>`.",
};
}
const raw = getHeader(params.req, "x-openclaw-model")?.trim();
if (!raw) {
return {};
}
const cfg = getRuntimeConfig();
const defaultModelRef = resolveDefaultModelForAgent({ cfg, agentId: params.agentId });
const defaultProvider = defaultModelRef.provider;
const manifestMetadataSnapshot = loadManifestMetadataSnapshot({
config: cfg,
env: process.env,
});
const modelManifestContext = {
manifestPlugins: manifestMetadataSnapshot.plugins,
};
const parsed = parseModelRef(raw, defaultProvider, {
allowManifestNormalization: true,
allowPluginNormalization: true,
...modelManifestContext,
});
if (!parsed) {
return { errorMessage: "Invalid `x-openclaw-model`." };
}
// Overrides must pass the same visibility policy as model picker surfaces;
// otherwise API clients could target hidden plugin/provider models by header.
const catalog = await loadGatewayModelCatalog();
const policy = createModelVisibilityPolicy({
cfg,
catalog,
defaultProvider,
agentId: params.agentId,
allowManifestNormalization: true,
allowPluginNormalization: true,
...modelManifestContext,
});
const normalized = modelKey(parsed.provider, parsed.model);
if (!policy.allowsKey(normalized)) {
return {
errorMessage: `Model '${normalized}' is not allowed for agent '${params.agentId}'.`,
};
}
return { modelOverride: raw };
}
/** Resolves the request agent from headers, model alias, or the configured default. */
export function resolveAgentIdForRequest(params: {
req: IncomingMessage;
model: string | undefined;
}): string {
const cfg = getRuntimeConfig();
const fromHeader = resolveAgentIdFromHeader(params.req);
if (fromHeader) {
assertKnownAgentId(fromHeader, cfg);
return fromHeader;
}
const fromModel = resolveAgentIdFromModel(params.model, cfg);
if (fromModel) {
assertKnownAgentId(fromModel, cfg);
return fromModel;
}
return resolveDefaultAgentId(cfg);
}
function resolveSessionKey(params: {
req: IncomingMessage;
agentId: string;
user?: string | undefined;
prefix: string;
}): string {
const explicit = getHeader(params.req, "x-openclaw-session-key")?.trim();
if (explicit) {
if (isReservedSessionKeyOverride(explicit, params.agentId)) {
throw new GatewaySessionKeyOverrideError();
}
return explicit;
}
const user = params.user?.trim();
const mainKey = user ? `${params.prefix}-user:${user}` : `${params.prefix}:${randomUUID()}`;
return buildAgentMainSessionKey({ agentId: params.agentId, mainKey });
}
function isReservedSessionKeyOverride(sessionKey: string, agentId: string): boolean {
const lowered = normalizeLowercaseStringOrEmpty(sessionKey);
const harnessLookupKey = sessionKey.startsWith("agent:")
? sessionKey
: canonicalizeSessionKeyForAgent(agentId, sessionKey);
const harnessEntry = isAgentHarnessSessionKey(sessionKey)
? resolveSessionEntryAccessTarget({
cfg: getRuntimeConfig(),
sessionKey: harnessLookupKey,
}).entry
: undefined;
const harnessKeyReserved =
isAgentHarnessSessionKey(sessionKey) &&
(!harnessEntry || isAgentHarnessSessionStoreEntryProtected(sessionKey, harnessEntry));
return (
lowered.startsWith("subagent:") ||
lowered.startsWith("cron:") ||
lowered.startsWith("acp:") ||
harnessKeyReserved ||
isSubagentSessionKey(sessionKey) ||
isCronSessionKey(sessionKey) ||
isAcpSessionKey(sessionKey)
);
}
/** Resolves gateway agent/session/channel context for OpenAI-compatible handlers. */
export function resolveGatewayRequestContext(params: {
req: IncomingMessage;
model: string | undefined;
user?: string | undefined;
sessionPrefix: string;
defaultMessageChannel: string;
useMessageChannelHeader?: boolean;
}): { agentId: string; sessionKey: string; messageChannel: string } {
const agentId = resolveAgentIdForRequest({ req: params.req, model: params.model });
const sessionKey = resolveSessionKey({
req: params.req,
agentId,
user: params.user,
prefix: params.sessionPrefix,
});
const messageChannel = params.useMessageChannelHeader
? (normalizeMessageChannel(getHeader(params.req, "x-openclaw-message-channel")) ??
params.defaultMessageChannel)
: params.defaultMessageChannel;
return { agentId, sessionKey, messageChannel };
}