Merge da944115659c545db83e85862bce48d03203657d into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Alex Alaniz 2026-03-21 02:34:59 +00:00 committed by GitHub
commit ffda520d6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 704 additions and 114 deletions

View File

@ -30,13 +30,15 @@ Notes:
## Security boundary (important)
Treat this endpoint as a **full operator-access** surface for the gateway instance.
Treat this endpoint as an **operator-access** surface for the gateway instance unless you
intentionally target a `publicMode` agent through a trusted upstream.
- HTTP bearer auth here is not a narrow per-user scope model.
- A valid Gateway token/password for this endpoint should be treated like an owner/operator credential.
- A valid Gateway token/password for this endpoint should still be treated like an owner/operator credential.
- Requests run through the same control-plane agent path as trusted operator actions.
- There is no separate non-owner/per-user tool boundary on this endpoint; once a caller passes Gateway auth here, OpenClaw treats that caller as a trusted operator for this gateway.
- If the target agent policy allows sensitive tools, this endpoint can use them.
- For normal agents, missing `x-openclaw-sender-is-owner` defaults to owner semantics.
- For agents with `publicMode: true`, missing `x-openclaw-sender-is-owner` defaults to non-owner semantics; only an explicit `x-openclaw-sender-is-owner: true` restores owner access.
- `publicMode` is not a full per-user sandbox. It redacts local runtime details in prompts and strips owner-only tools, but any non-owner tools still allowed by the target agent remain callable.
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
See [Security](/gateway/security) and [Remote access](/gateway/remote).

View File

@ -29,6 +29,74 @@ Operational behavior matches [OpenAI Chat Completions](/gateway/openai-http-api)
Enable or disable this endpoint with `gateway.http.endpoints.responses.enabled`.
Notes:
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`).
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `OPENCLAW_GATEWAY_PASSWORD`).
- If `gateway.auth.rateLimit` is configured and too many auth failures occur, the endpoint returns `429` with `Retry-After`.
## Security boundary (important)
Treat this endpoint as an **operator-access** surface for the gateway instance unless you
intentionally target a `publicMode` agent through a trusted upstream.
- HTTP bearer auth here is not a narrow per-user scope model.
- A valid Gateway token/password for this endpoint should still be treated like an owner/operator credential.
- Requests run through the same control-plane agent path as trusted operator actions.
- For normal agents, missing `x-openclaw-sender-is-owner` defaults to owner semantics.
- For agents with `publicMode: true`, missing `x-openclaw-sender-is-owner` defaults to non-owner semantics; only an explicit `x-openclaw-sender-is-owner: true` restores owner access.
- `publicMode` is not a full per-user sandbox. It redacts local runtime details in prompts and strips owner-only tools, but any non-owner tools still allowed by the target agent remain callable.
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
See [Security](/gateway/security) and [Remote access](/gateway/remote).
## Choosing an agent
No custom headers required: encode the agent id in the OpenResponses `model` field:
- `model: "openclaw:<agentId>"` (example: `"openclaw:main"`, `"openclaw:beta"`)
- `model: "agent:<agentId>"` (alias)
Or target a specific OpenClaw agent by header:
- `x-openclaw-agent-id: <agentId>` (default: `main`)
Advanced:
- `x-openclaw-session-key: <sessionKey>` to fully control session routing.
## Enabling the endpoint
Set `gateway.http.endpoints.responses.enabled` to `true`:
```json5
{
gateway: {
http: {
endpoints: {
responses: { enabled: true },
},
},
},
}
```
## Disabling the endpoint
Set `gateway.http.endpoints.responses.enabled` to `false`:
```json5
{
gateway: {
http: {
endpoints: {
responses: { enabled: false },
},
},
},
}
```
## Session behavior
By default the endpoint is **stateless per request** (a new session key is generated each call).

View File

@ -803,8 +803,9 @@ still require token/password auth.
Important boundary note:
- Gateway HTTP bearer auth is effectively all-or-nothing operator access.
- Gateway HTTP bearer auth is effectively operator access for the gateway.
- Treat credentials that can call `/v1/chat/completions`, `/v1/responses`, `/tools/invoke`, or `/api/channels/*` as full-access operator secrets for that gateway.
- For `/v1/chat/completions` and `/v1/responses`, agents with `publicMode: true` default missing `x-openclaw-sender-is-owner` to non-owner semantics, but that only strips owner-only tools and prompt metadata. It is not a full per-user sandbox.
- Do not share these credentials with untrusted callers; prefer separate gateways per trust boundary.
**Trust assumption:** tokenless Serve auth assumes the gateway host is trusted.

View File

@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { resolveAgentPublicMode } from "./agent-scope.js";
describe("resolveAgentPublicMode", () => {
it("inherits agents.defaults.publicMode when an agent has no explicit override", () => {
expect(
resolveAgentPublicMode(
{
agents: {
defaults: { publicMode: true },
list: [{ id: "public-agent" }],
},
},
"public-agent",
),
).toBe(true);
});
it("lets an explicit per-agent false override a default true value", () => {
expect(
resolveAgentPublicMode(
{
agents: {
defaults: { publicMode: true },
list: [{ id: "private-agent", publicMode: false }],
},
},
"private-agent",
),
).toBe(false);
});
});

View File

@ -27,6 +27,7 @@ type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[num
type ResolvedAgentConfig = {
name?: string;
publicMode?: boolean;
workspace?: string;
agentDir?: string;
model?: AgentEntry["model"];
@ -124,8 +125,15 @@ export function resolveAgentConfig(
if (!entry) {
return undefined;
}
const defaultPublicMode = cfg.agents?.defaults?.publicMode;
return {
name: typeof entry.name === "string" ? entry.name : undefined,
publicMode:
typeof entry.publicMode === "boolean"
? entry.publicMode
: typeof defaultPublicMode === "boolean"
? defaultPublicMode
: undefined,
workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
model:
@ -144,6 +152,10 @@ export function resolveAgentConfig(
};
}
export function resolveAgentPublicMode(cfg: OpenClawConfig, agentId: string): boolean {
return resolveAgentConfig(cfg, agentId)?.publicMode === true;
}
export function resolveAgentSkillsFilter(
cfg: OpenClawConfig,
agentId: string,

View File

@ -10,6 +10,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { CliBackendConfig } from "../../config/types.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { isRecord } from "../../utils.js";
import { resolveAgentPublicMode, resolveDefaultAgentId } from "../agent-scope.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
import { resolveDefaultModelForAgent } from "../model-selection.js";
import { resolveOwnerDisplaySetting } from "../owner-display.js";
@ -51,14 +52,21 @@ export function buildSystemPrompt(params: {
modelDisplay: string;
agentId?: string;
}) {
const resolvedAgentId =
params.agentId ?? (params.config ? resolveDefaultAgentId(params.config) : undefined);
const publicMode =
params.config && resolvedAgentId
? resolveAgentPublicMode(params.config, resolvedAgentId)
: false;
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.config ?? {},
agentId: params.agentId,
agentId: resolvedAgentId,
});
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: params.agentId,
agentId: resolvedAgentId,
publicMode,
workspaceDir: params.workspaceDir,
cwd: process.cwd(),
runtime: {
@ -75,6 +83,7 @@ export function buildSystemPrompt(params: {
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
return buildAgentSystemPrompt({
workspaceDir: params.workspaceDir,
publicMode,
defaultThinkLevel: params.defaultThinkLevel,
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,

View File

@ -34,7 +34,7 @@ import { resolveUserPath } from "../../../utils.js";
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
import { resolveOpenClawAgentDir } from "../../agent-paths.js";
import { resolveSessionAgentIds } from "../../agent-scope.js";
import { resolveAgentPublicMode, resolveSessionAgentIds } from "../../agent-scope.js";
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
import { createAnthropicVertexStreamFnForModel } from "../../anthropic-vertex-stream.js";
import {
@ -1672,13 +1672,6 @@ export async function runEmbeddedAttempt(
config: params.config,
});
const skillsPrompt = resolveSkillsPromptForRun({
skillsSnapshot: params.skillsSnapshot,
entries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});
const sessionLabel = params.sessionKey ?? params.sessionId;
const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles } =
await resolveBootstrapContextForRun({
@ -1720,6 +1713,15 @@ export async function runEmbeddedAttempt(
config: params.config,
agentId: params.agentId,
});
const publicMode = resolveAgentPublicMode(params.config ?? {}, sessionAgentId);
const skillsPrompt = publicMode
? ""
: resolveSkillsPromptForRun({
skillsSnapshot: params.skillsSnapshot,
entries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
config: params.config,
sessionAgentId,
@ -1909,6 +1911,7 @@ export async function runEmbeddedAttempt(
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: sessionAgentId,
publicMode,
workspaceDir: effectiveWorkspace,
cwd: process.cwd(),
runtime: {
@ -1940,6 +1943,7 @@ export async function runEmbeddedAttempt(
const appendPrompt = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace,
publicMode,
defaultThinkLevel: params.thinkLevel,
reasoningLevel: params.reasoningLevel ?? "off",
extraSystemPrompt: params.extraSystemPrompt,

View File

@ -10,6 +10,7 @@ import type { ReasoningLevel, ThinkLevel } from "./utils.js";
export function buildEmbeddedSystemPrompt(params: {
workspaceDir: string;
publicMode?: boolean;
defaultThinkLevel?: ThinkLevel;
reasoningLevel?: ReasoningLevel;
extraSystemPrompt?: string;
@ -32,11 +33,11 @@ export function buildEmbeddedSystemPrompt(params: {
acpEnabled?: boolean;
runtimeInfo: {
agentId?: string;
host: string;
os: string;
arch: string;
node: string;
model: string;
host?: string;
os?: string;
arch?: string;
node?: string;
model?: string;
provider?: string;
capabilities?: string[];
channel?: string;
@ -55,6 +56,7 @@ export function buildEmbeddedSystemPrompt(params: {
}): string {
return buildAgentSystemPrompt({
workspaceDir: params.workspaceDir,
publicMode: params.publicMode,
defaultThinkLevel: params.defaultThinkLevel,
reasoningLevel: params.reasoningLevel,
extraSystemPrompt: params.extraSystemPrompt,

View File

@ -13,9 +13,15 @@ async function makeRepoRoot(root: string): Promise<void> {
await fs.mkdir(path.join(root, ".git"), { recursive: true });
}
function buildParams(params: { config?: OpenClawConfig; workspaceDir?: string; cwd?: string }) {
function buildParams(params: {
config?: OpenClawConfig;
publicMode?: boolean;
workspaceDir?: string;
cwd?: string;
}) {
return buildSystemPromptParams({
config: params.config,
publicMode: params.publicMode,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
runtime: {
@ -101,4 +107,22 @@ describe("buildSystemPromptParams repo root", () => {
expect(runtimeInfo.repoRoot).toBeUndefined();
});
it("strips sensitive runtime fields in public mode", async () => {
const temp = await makeTempDir("public");
const repoRoot = path.join(temp, "repo");
const workspaceDir = path.join(repoRoot, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
await makeRepoRoot(repoRoot);
const { runtimeInfo } = buildParams({ publicMode: true, workspaceDir });
expect(runtimeInfo.host).toBeUndefined();
expect(runtimeInfo.os).toBeUndefined();
expect(runtimeInfo.arch).toBeUndefined();
expect(runtimeInfo.repoRoot).toBeUndefined();
expect(runtimeInfo.shell).toBeUndefined();
expect(runtimeInfo.node).toBe("node");
expect(runtimeInfo.model).toBe("model");
});
});

View File

@ -11,11 +11,11 @@ import {
export type RuntimeInfoInput = {
agentId?: string;
host: string;
os: string;
arch: string;
node: string;
model: string;
host?: string;
os?: string;
arch?: string;
node?: string;
model?: string;
defaultModel?: string;
shell?: string;
channel?: string;
@ -35,6 +35,7 @@ export type SystemPromptRuntimeParams = {
export function buildSystemPromptParams(params: {
config?: OpenClawConfig;
agentId?: string;
publicMode?: boolean;
runtime: Omit<RuntimeInfoInput, "agentId">;
workspaceDir?: string;
cwd?: string;
@ -47,12 +48,20 @@ export function buildSystemPromptParams(params: {
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
const runtimeInfo: RuntimeInfoInput = {
agentId: params.agentId,
...params.runtime,
repoRoot,
};
if (params.publicMode) {
runtimeInfo.host = undefined;
runtimeInfo.os = undefined;
runtimeInfo.arch = undefined;
runtimeInfo.repoRoot = undefined;
runtimeInfo.shell = undefined;
}
return {
runtimeInfo: {
agentId: params.agentId,
...params.runtime,
repoRoot,
},
runtimeInfo,
userTimezone,
userTime,
userTimeFormat,

View File

@ -637,6 +637,47 @@ describe("buildAgentSystemPrompt", () => {
expect(line).toContain("thinking=low");
});
it("redacts runtime and workspace details in public mode", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/Users/alex/openclaw",
publicMode: true,
docsPath: "/Users/alex/openclaw/docs",
runtimeInfo: {
agentId: "community",
host: "Alexs-Mac-Mini",
repoRoot: "/Users/alex/openclaw",
os: "Darwin 25.3.0",
arch: "arm64",
node: "v25.2.1",
model: "openai/gpt-5",
defaultModel: "openai/gpt-5",
shell: "/bin/zsh",
},
sandboxInfo: {
enabled: true,
workspaceDir: "/Users/alex/openclaw",
containerWorkspaceDir: "/workspace",
workspaceAccess: "rw",
agentWorkspaceMount: "/mnt/agent",
browserNoVncUrl: "http://internal.example/vnc",
elevated: { allowed: true, defaultLevel: "ask" },
},
});
expect(prompt).toContain("Runtime: public-facing agent");
expect(prompt).toContain(
"Your working directory is: managed internally for this public-facing agent",
);
expect(prompt).toContain("Agent workspace access: rw");
expect(prompt).not.toContain("agent=community");
expect(prompt).not.toContain("host=Alexs-Mac-Mini");
expect(prompt).not.toContain("model=openai/gpt-5");
expect(prompt).not.toContain("OpenClaw docs: /Users/alex/openclaw/docs");
expect(prompt).not.toContain("/Users/alex/openclaw");
expect(prompt).not.toContain("/workspace");
expect(prompt).not.toContain("http://internal.example/vnc");
});
it("describes sandboxed runtime and elevated when allowed", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",

View File

@ -168,14 +168,21 @@ function buildVoiceSection(params: { isMinimal: boolean; ttsHint?: string }) {
return ["## Voice (TTS)", hint, ""];
}
function buildDocsSection(params: { docsPath?: string; isMinimal: boolean; readToolName: string }) {
function buildDocsSection(params: {
docsPath?: string;
isMinimal: boolean;
publicMode?: boolean;
readToolName: string;
}) {
const docsPath = params.docsPath?.trim();
if (!docsPath || params.isMinimal) {
return [];
}
return [
"## Documentation",
`OpenClaw docs: ${docsPath}`,
const lines = ["## Documentation"];
if (!params.publicMode) {
lines.push(`OpenClaw docs: ${docsPath}`);
}
lines.push(
"Mirror: https://docs.openclaw.ai",
"Source: https://github.com/openclaw/openclaw",
"Community: https://discord.com/invite/clawd",
@ -183,11 +190,13 @@ function buildDocsSection(params: { docsPath?: string; isMinimal: boolean; readT
"For OpenClaw behavior, commands, config, or architecture: consult local docs first.",
"When diagnosing issues, run `openclaw status` yourself when possible; only ask the user if you lack access (e.g., sandboxed).",
"",
];
);
return lines;
}
export function buildAgentSystemPrompt(params: {
workspaceDir: string;
publicMode?: boolean;
defaultThinkLevel?: ThinkLevel;
reasoningLevel?: ReasoningLevel;
extraSystemPrompt?: string;
@ -233,6 +242,7 @@ export function buildAgentSystemPrompt(params: {
};
memoryCitationsMode?: MemoryCitationsMode;
}) {
const publicMode = params.publicMode === true;
const acpEnabled = params.acpEnabled !== false;
const sandboxedRuntime = params.sandboxInfo?.enabled === true;
const acpSpawnRuntimeEnabled = acpEnabled && !sandboxedRuntime;
@ -384,14 +394,60 @@ export function buildAgentSystemPrompt(params: {
const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace
? sanitizeForPromptLiteral(sandboxContainerWorkspace)
: "";
const displayWorkspaceDir =
params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
const displayWorkspaceDir = publicMode
? "managed internally for this public-facing agent"
: params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
? sanitizedSandboxContainerWorkspace
: sanitizedWorkspaceDir;
const workspaceGuidance =
params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
const workspaceGuidance = publicMode
? "Prefer relative paths for file operations. Do not rely on host-specific absolute paths unless the user explicitly provides them."
: params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
? `For read/write/edit/apply_patch, file paths resolve against host workspace: ${sanitizedWorkspaceDir}. For bash/exec commands, use sandbox container paths under ${sanitizedSandboxContainerWorkspace} (or relative paths from that workdir), not host paths. Prefer relative paths so both sandboxed exec and file tools work consistently.`
: "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.";
const sandboxSection = params.sandboxInfo?.enabled
? [
"You are running in a sandboxed runtime (tools execute in Docker).",
"Some tools may be unavailable due to sandbox policy.",
"Sub-agents stay sandboxed (no elevated/host access). Need outside-sandbox read/write? Don't spawn; ask first.",
hasSessionsSpawn && acpEnabled
? 'ACP harness spawns are blocked from sandboxed sessions (`sessions_spawn` with `runtime: "acp"`). Use `runtime: "subagent"` instead.'
: "",
!publicMode && params.sandboxInfo.containerWorkspaceDir
? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
: "",
!publicMode && params.sandboxInfo.workspaceDir
? `Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
: "",
params.sandboxInfo.workspaceAccess
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
!publicMode && params.sandboxInfo.agentWorkspaceMount
? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
: ""
}`
: "",
params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
!publicMode && params.sandboxInfo.browserNoVncUrl
? `Sandbox browser observer (noVNC): ${sanitizeForPromptLiteral(params.sandboxInfo.browserNoVncUrl)}`
: "",
params.sandboxInfo.hostBrowserAllowed === true
? "Host browser control: allowed."
: params.sandboxInfo.hostBrowserAllowed === false
? "Host browser control: blocked."
: "",
params.sandboxInfo.elevated?.allowed ? "Elevated exec is available for this session." : "",
params.sandboxInfo.elevated?.allowed
? "User can toggle with /elevated on|off|ask|full."
: "",
params.sandboxInfo.elevated?.allowed
? "You may also send /elevated on|off|ask|full when needed."
: "",
params.sandboxInfo.elevated?.allowed
? `Current elevated level: ${params.sandboxInfo.elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
: "",
]
.filter(Boolean)
.join("\n")
: "";
const safetySection = [
"## Safety",
"You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.",
@ -411,6 +467,7 @@ export function buildAgentSystemPrompt(params: {
const docsSection = buildDocsSection({
docsPath: params.docsPath,
isMinimal,
publicMode,
readToolName,
});
const workspaceNotes = (params.workspaceNotes ?? []).map((note) => note.trim()).filter(Boolean);
@ -515,52 +572,7 @@ export function buildAgentSystemPrompt(params: {
"",
...docsSection,
params.sandboxInfo?.enabled ? "## Sandbox" : "",
params.sandboxInfo?.enabled
? [
"You are running in a sandboxed runtime (tools execute in Docker).",
"Some tools may be unavailable due to sandbox policy.",
"Sub-agents stay sandboxed (no elevated/host access). Need outside-sandbox read/write? Don't spawn; ask first.",
hasSessionsSpawn && acpEnabled
? 'ACP harness spawns are blocked from sandboxed sessions (`sessions_spawn` with `runtime: "acp"`). Use `runtime: "subagent"` instead.'
: "",
params.sandboxInfo.containerWorkspaceDir
? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
: "",
params.sandboxInfo.workspaceDir
? `Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
: "",
params.sandboxInfo.workspaceAccess
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
params.sandboxInfo.agentWorkspaceMount
? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
: ""
}`
: "",
params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
params.sandboxInfo.browserNoVncUrl
? `Sandbox browser observer (noVNC): ${sanitizeForPromptLiteral(params.sandboxInfo.browserNoVncUrl)}`
: "",
params.sandboxInfo.hostBrowserAllowed === true
? "Host browser control: allowed."
: params.sandboxInfo.hostBrowserAllowed === false
? "Host browser control: blocked."
: "",
params.sandboxInfo.elevated?.allowed
? "Elevated exec is available for this session."
: "",
params.sandboxInfo.elevated?.allowed
? "User can toggle with /elevated on|off|ask|full."
: "",
params.sandboxInfo.elevated?.allowed
? "You may also send /elevated on|off|ask|full when needed."
: "",
params.sandboxInfo.elevated?.allowed
? `Current elevated level: ${params.sandboxInfo.elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
: "",
]
.filter(Boolean)
.join("\n")
: "",
sandboxSection,
params.sandboxInfo?.enabled ? "" : "",
...buildUserIdentitySection(ownerLine, isMinimal),
...buildTimeSection({
@ -672,7 +684,13 @@ export function buildAgentSystemPrompt(params: {
lines.push(
"## Runtime",
buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel),
buildRuntimeLine(
runtimeInfo,
runtimeChannel,
runtimeCapabilities,
params.defaultThinkLevel,
publicMode,
),
`Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`,
);
@ -694,7 +712,11 @@ export function buildRuntimeLine(
runtimeChannel?: string,
runtimeCapabilities: string[] = [],
defaultThinkLevel?: ThinkLevel,
publicMode?: boolean,
): string {
if (publicMode) {
return "Runtime: public-facing agent";
}
return `Runtime: ${[
runtimeInfo?.agentId ? `agent=${runtimeInfo.agentId}` : "",
runtimeInfo?.host ? `host=${runtimeInfo.host}` : "",

View File

@ -1,5 +1,5 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
import { resolveAgentPublicMode, resolveSessionAgentIds } from "../../agents/agent-scope.js";
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
import type { EmbeddedContextFile } from "../../agents/pi-embedded-helpers.js";
@ -28,6 +28,13 @@ export async function resolveCommandsSystemPromptBundle(
params: HandleCommandsParams,
): Promise<CommandsSystemPromptBundle> {
const workspaceDir = params.workspaceDir;
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.cfg,
agentId: params.agentId,
});
const publicMode =
params.cfg && sessionAgentId ? resolveAgentPublicMode(params.cfg, sessionAgentId) : false;
const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({
workspaceDir,
config: params.cfg,
@ -35,6 +42,9 @@ export async function resolveCommandsSystemPromptBundle(
sessionId: params.sessionEntry?.sessionId,
});
const skillsSnapshot = (() => {
if (publicMode) {
return { prompt: "", skills: [], resolvedSkills: [] };
}
try {
return buildWorkspaceSkillSnapshot(workspaceDir, {
config: params.cfg,
@ -73,11 +83,6 @@ export async function resolveCommandsSystemPromptBundle(
})();
const toolSummaries = buildToolSummaryMap(tools);
const toolNames = tools.map((t) => t.name);
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.cfg,
agentId: params.agentId,
});
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.cfg,
agentId: sessionAgentId,
@ -86,6 +91,7 @@ export async function resolveCommandsSystemPromptBundle(
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.cfg,
agentId: sessionAgentId,
publicMode,
workspaceDir,
cwd: process.cwd(),
runtime: {
@ -112,6 +118,7 @@ export async function resolveCommandsSystemPromptBundle(
const systemPrompt = buildAgentSystemPrompt({
workspaceDir,
publicMode,
defaultThinkLevel: params.resolvedThinkLevel,
reasoningLevel: params.resolvedReasoningLevel,
extraSystemPrompt: undefined,

View File

@ -230,6 +230,10 @@ export const FIELD_HELP: Record<string, string> = {
"Optional default working directory for this agent's ACP sessions.",
"agents.list[].identity.avatar":
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
"agents.defaults.publicMode":
"When true, redact local runtime/workspace details in prompts and default HTTP /v1 ingress for public-mode agents to non-owner semantics unless x-openclaw-sender-is-owner=true is explicitly supplied by a trusted upstream.",
"agents.list[].publicMode":
"Per-agent public-mode override. Public-mode agents hide local runtime details in prompts and default HTTP /v1 ingress to non-owner semantics unless a trusted upstream explicitly marks the caller as owner.",
"agents.defaults.heartbeat.suppressToolErrorWarnings":
"Suppress tool error warning payloads during heartbeat runs.",
"agents.list[].heartbeat.suppressToolErrorWarnings":

View File

@ -65,7 +65,9 @@ export const FIELD_LABELS: Record<string, string> = {
"agents.list[].runtime.acp.cwd": "Agent ACP Working Directory",
agents: "Agents",
"agents.defaults": "Agent Defaults",
"agents.defaults.publicMode": "Agent Default Public Mode",
"agents.list": "Agent List",
"agents.list[].publicMode": "Agent Public Mode",
gateway: "Gateway",
"gateway.port": "Gateway Port",
"gateway.mode": "Gateway Mode",

View File

@ -120,6 +120,8 @@ export type CliBackendConfig = {
export type AgentDefaultsConfig = {
/** Primary model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */
model?: AgentModelConfig;
/** Hide environment/runtime details from prompts for public-facing agents by default. */
publicMode?: boolean;
/** Optional image-capable model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */
imageModel?: AgentModelConfig;
/** Optional image-generation model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */

View File

@ -62,6 +62,8 @@ export type AgentConfig = {
id: string;
default?: boolean;
name?: string;
/** Hide environment/runtime details from prompts for public-facing agents. */
publicMode?: boolean;
workspace?: string;
agentDir?: string;
model?: AgentModelConfig;

View File

@ -17,6 +17,7 @@ import {
export const AgentDefaultsSchema = z
.object({
model: AgentModelSchema.optional(),
publicMode: z.boolean().optional(),
imageModel: AgentModelSchema.optional(),
imageGenerationModel: AgentModelSchema.optional(),
pdfModel: AgentModelSchema.optional(),

View File

@ -764,6 +764,7 @@ export const AgentEntrySchema = z
id: z.string(),
default: z.boolean().optional(),
name: z.string().optional(),
publicMode: z.boolean().optional(),
workspace: z.string().optional(),
agentDir: z.string().optional(),
model: AgentModelSchema.optional(),

View File

@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import type { IncomingMessage } from "node:http";
import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { isTrustedProxyAddress } from "./net.js";
export function getHeader(req: IncomingMessage, name: string): string | undefined {
const raw = req.headers[name.toLowerCase()];
@ -23,6 +24,21 @@ export function getBearerToken(req: IncomingMessage): string | undefined {
return token || undefined;
}
export function resolveIngressSenderIsOwner(params: {
req: IncomingMessage;
publicMode?: boolean;
trustedProxies?: string[];
}): boolean {
const raw = getHeader(params.req, "x-openclaw-sender-is-owner")?.trim().toLowerCase();
if (raw === "true") {
return isTrustedProxyAddress(params.req.socket?.remoteAddress, params.trustedProxies);
}
if (raw === "false") {
return false;
}
return params.publicMode !== true;
}
export function resolveAgentIdFromHeader(req: IncomingMessage): string | undefined {
const raw =
getHeader(req, "x-openclaw-agent-id")?.trim() ||

View File

@ -65,4 +65,71 @@ describe("openai image budget accounting", () => {
},
]);
});
it("honors x-openclaw-sender-is-owner=false", () => {
const command = __testOnlyOpenAiHttp.buildAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "false",
},
} as never,
prompt: { message: "hi" },
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
});
expect(command.senderIsOwner).toBe(false);
});
it("defaults public-mode ingress to non-owner when the header is missing", () => {
const command = __testOnlyOpenAiHttp.buildAgentCommandInput({
req: { headers: {} } as never,
prompt: { message: "hi" },
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
});
expect(command.senderIsOwner).toBe(false);
});
it("ignores x-openclaw-sender-is-owner=true from an untrusted caller", () => {
const command = __testOnlyOpenAiHttp.buildAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "true",
},
socket: { remoteAddress: "203.0.113.9" },
} as never,
prompt: { message: "hi" },
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
trustedProxies: ["127.0.0.1"],
});
expect(command.senderIsOwner).toBe(false);
});
it("honors x-openclaw-sender-is-owner=true from a trusted proxy", () => {
const command = __testOnlyOpenAiHttp.buildAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "true",
},
socket: { remoteAddress: "127.0.0.1" },
} as never,
prompt: { message: "hi" },
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
trustedProxies: ["127.0.0.1"],
});
expect(command.senderIsOwner).toBe(true);
});
});

View File

@ -636,6 +636,65 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
);
});
it("defaults public-mode agents to non-owner ingress unless explicitly trusted", async () => {
testState.agentsConfig = {
defaults: { publicMode: true },
list: [
{ id: "beta", publicMode: false },
{ id: "gamma", publicMode: true },
],
};
agentCommand.mockClear();
agentCommand.mockResolvedValue({ payloads: [{ text: "hello" }] } as never);
const defaultRes = await postChatCompletions(enabledPort, {
model: "openclaw",
messages: [{ role: "user", content: "hi" }],
});
expect(defaultRes.status).toBe(200);
const defaultOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(defaultOpts?.senderIsOwner).toBe(true);
await defaultRes.text();
agentCommand.mockClear();
const publicRes = await postChatCompletions(
enabledPort,
{
model: "openclaw",
messages: [{ role: "user", content: "hi" }],
},
{ "x-openclaw-agent-id": "gamma" },
);
expect(publicRes.status).toBe(200);
const publicOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(publicOpts?.senderIsOwner).toBe(false);
await publicRes.text();
agentCommand.mockClear();
const routedPublicRes = await postChatCompletions(
enabledPort,
{
model: "openclaw",
messages: [{ role: "user", content: "hi" }],
},
{
"x-openclaw-agent-id": "beta",
"x-openclaw-session-key": "agent:gamma:openai:routed-public",
},
);
expect(routedPublicRes.status).toBe(200);
const routedPublicOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(routedPublicOpts?.senderIsOwner).toBe(false);
await routedPublicRes.text();
});
it("streams SSE chunks when stream=true", async () => {
const port = enabledPort;
try {

View File

@ -1,8 +1,10 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { resolveAgentPublicMode, resolveSessionAgentId } from "../agents/agent-scope.js";
import type { ImageContent } from "../agents/command/types.js";
import { createDefaultDeps } from "../cli/deps.js";
import { agentCommandFromIngress } from "../commands/agent.js";
import type { OpenClawConfig } from "../config/config.js";
import type { GatewayHttpChatCompletionsConfig } from "../config/types.gateway.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
import { logWarn } from "../logger.js";
@ -27,12 +29,13 @@ import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
import { resolveGatewayRequestContext } from "./http-utils.js";
import { resolveGatewayRequestContext, resolveIngressSenderIsOwner } from "./http-utils.js";
import { normalizeInputHostnameAllowlist } from "./input-allowlist.js";
type OpenAiHttpOptions = {
auth: ResolvedGatewayAuth;
config?: GatewayHttpChatCompletionsConfig;
runtimeConfig?: OpenClawConfig;
maxBodyBytes?: number;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
@ -101,11 +104,19 @@ function writeSse(res: ServerResponse, data: unknown) {
}
function buildAgentCommandInput(params: {
req: IncomingMessage;
prompt: { message: string; extraSystemPrompt?: string; images?: ImageContent[] };
sessionKey: string;
runId: string;
messageChannel: string;
publicMode?: boolean;
trustedProxies?: string[];
}) {
const senderIsOwner = resolveIngressSenderIsOwner({
req: params.req,
publicMode: params.publicMode,
trustedProxies: params.trustedProxies,
});
return {
message: params.prompt.message,
extraSystemPrompt: params.prompt.extraSystemPrompt,
@ -115,8 +126,7 @@ function buildAgentCommandInput(params: {
deliver: false as const,
messageChannel: params.messageChannel,
bestEffortDeliver: false as const,
// HTTP API callers are authenticated operator clients for this gateway context.
senderIsOwner: true as const,
senderIsOwner,
allowModelOverride: true as const,
};
}
@ -315,6 +325,7 @@ async function resolveImagesForRequest(
}
export const __testOnlyOpenAiHttp = {
buildAgentCommandInput,
resolveImagesForRequest,
resolveOpenAiChatCompletionsLimits,
};
@ -440,6 +451,9 @@ export async function handleOpenAiHttpRequest(
defaultMessageChannel: "webchat",
useMessageChannelHeader: true,
});
const runtimeConfig = opts.runtimeConfig ?? {};
const effectiveAgentId = resolveSessionAgentId({ sessionKey, config: runtimeConfig });
const publicMode = resolveAgentPublicMode(runtimeConfig, effectiveAgentId);
const activeTurnContext = resolveActiveTurnContext(payload.messages);
const prompt = buildAgentPrompt(payload.messages, activeTurnContext.activeUserMessageIndex);
let images: ImageContent[] = [];
@ -469,6 +483,7 @@ export async function handleOpenAiHttpRequest(
const runId = `chatcmpl_${randomUUID()}`;
const deps = createDefaultDeps();
const commandInput = buildAgentCommandInput({
req,
prompt: {
message: prompt.message,
extraSystemPrompt: prompt.extraSystemPrompt,
@ -477,6 +492,8 @@ export async function handleOpenAiHttpRequest(
sessionKey,
runId,
messageChannel,
publicMode,
trustedProxies: opts.trustedProxies,
});
if (!stream) {

View File

@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import { __testOnlyOpenResponsesHttp } from "./openresponses-http.js";
describe("openresponses owner header handling", () => {
it("honors x-openclaw-sender-is-owner=false", () => {
const command = __testOnlyOpenResponsesHttp.buildResponsesAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "false",
},
} as never,
message: "hi",
images: [],
clientTools: [],
extraSystemPrompt: "",
streamParams: undefined,
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
});
expect(command.senderIsOwner).toBe(false);
});
it("defaults public-mode ingress to non-owner when the header is missing", () => {
const command = __testOnlyOpenResponsesHttp.buildResponsesAgentCommandInput({
req: { headers: {} } as never,
message: "hi",
images: [],
clientTools: [],
extraSystemPrompt: "",
streamParams: undefined,
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
});
expect(command.senderIsOwner).toBe(false);
});
it("ignores x-openclaw-sender-is-owner=true from an untrusted caller", () => {
const command = __testOnlyOpenResponsesHttp.buildResponsesAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "true",
},
socket: { remoteAddress: "203.0.113.9" },
} as never,
message: "hi",
images: [],
clientTools: [],
extraSystemPrompt: "",
streamParams: undefined,
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
trustedProxies: ["127.0.0.1"],
});
expect(command.senderIsOwner).toBe(false);
});
it("honors x-openclaw-sender-is-owner=true from a trusted proxy", () => {
const command = __testOnlyOpenResponsesHttp.buildResponsesAgentCommandInput({
req: {
headers: {
"x-openclaw-sender-is-owner": "true",
},
socket: { remoteAddress: "127.0.0.1" },
} as never,
message: "hi",
images: [],
clientTools: [],
extraSystemPrompt: "",
streamParams: undefined,
sessionKey: "agent:main:test",
runId: "run-1",
messageChannel: "webchat",
publicMode: true,
trustedProxies: ["127.0.0.1"],
});
expect(command.senderIsOwner).toBe(true);
});
});

View File

@ -5,7 +5,7 @@ import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js";
import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js";
import { emitAgentEvent } from "../infra/agent-events.js";
import { buildAssistantDeltaResult } from "./test-helpers.agent-results.js";
import { agentCommand, getFreePort, installGatewayTestHooks } from "./test-helpers.js";
import { agentCommand, getFreePort, installGatewayTestHooks, testState } from "./test-helpers.js";
installGatewayTestHooks({ scope: "suite" });
@ -534,6 +534,65 @@ describe("OpenResponses HTTP API (e2e)", () => {
}
});
it("defaults public-mode agents to non-owner ingress unless explicitly trusted", async () => {
testState.agentsConfig = {
defaults: { publicMode: true },
list: [
{ id: "beta", publicMode: false },
{ id: "gamma", publicMode: true },
],
};
agentCommand.mockClear();
agentCommand.mockResolvedValue({ payloads: [{ text: "hello" }] } as never);
const defaultRes = await postResponses(enabledPort, {
model: "openclaw",
input: "hi",
});
expect(defaultRes.status).toBe(200);
const defaultOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(defaultOpts?.senderIsOwner).toBe(true);
await ensureResponseConsumed(defaultRes);
agentCommand.mockClear();
const publicRes = await postResponses(
enabledPort,
{
model: "openclaw",
input: "hi",
},
{ "x-openclaw-agent-id": "gamma" },
);
expect(publicRes.status).toBe(200);
const publicOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(publicOpts?.senderIsOwner).toBe(false);
await ensureResponseConsumed(publicRes);
agentCommand.mockClear();
const routedPublicRes = await postResponses(
enabledPort,
{
model: "openclaw",
input: "hi",
},
{
"x-openclaw-agent-id": "beta",
"x-openclaw-session-key": "agent:gamma:openresponses:routed-public",
},
);
expect(routedPublicRes.status).toBe(200);
const routedPublicOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
| { senderIsOwner?: boolean }
| undefined;
expect(routedPublicOpts?.senderIsOwner).toBe(false);
await ensureResponseConsumed(routedPublicRes);
});
it("streams OpenResponses SSE events", async () => {
const port = enabledPort;
try {

View File

@ -8,10 +8,12 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { resolveAgentPublicMode, resolveSessionAgentId } from "../agents/agent-scope.js";
import type { ImageContent } from "../agents/command/types.js";
import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/params.js";
import { createDefaultDeps } from "../cli/deps.js";
import { agentCommandFromIngress } from "../commands/agent.js";
import type { OpenClawConfig } from "../config/config.js";
import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
import { logWarn } from "../logger.js";
@ -35,7 +37,7 @@ import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
import { resolveGatewayRequestContext } from "./http-utils.js";
import { resolveGatewayRequestContext, resolveIngressSenderIsOwner } from "./http-utils.js";
import { normalizeInputHostnameAllowlist } from "./input-allowlist.js";
import {
CreateResponseBodySchema,
@ -51,6 +53,7 @@ type OpenResponsesHttpOptions = {
auth: ResolvedGatewayAuth;
maxBodyBytes?: number;
config?: GatewayHttpResponsesConfig;
runtimeConfig?: OpenClawConfig;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
@ -232,7 +235,8 @@ function createAssistantOutputItem(params: {
};
}
async function runResponsesAgentCommand(params: {
function buildResponsesAgentCommandInput(params: {
req: IncomingMessage;
message: string;
images: ImageContent[];
clientTools: ClientToolDefinition[];
@ -241,29 +245,55 @@ async function runResponsesAgentCommand(params: {
sessionKey: string;
runId: string;
messageChannel: string;
publicMode?: boolean;
trustedProxies?: string[];
}) {
const senderIsOwner = resolveIngressSenderIsOwner({
req: params.req,
publicMode: params.publicMode,
trustedProxies: params.trustedProxies,
});
return {
message: params.message,
images: params.images.length > 0 ? params.images : undefined,
clientTools: params.clientTools.length > 0 ? params.clientTools : undefined,
extraSystemPrompt: params.extraSystemPrompt || undefined,
streamParams: params.streamParams ?? undefined,
sessionKey: params.sessionKey,
runId: params.runId,
deliver: false,
messageChannel: params.messageChannel,
bestEffortDeliver: false,
senderIsOwner,
};
}
async function runResponsesAgentCommand(params: {
req: IncomingMessage;
message: string;
images: ImageContent[];
clientTools: ClientToolDefinition[];
extraSystemPrompt: string;
streamParams: { maxTokens: number } | undefined;
sessionKey: string;
runId: string;
messageChannel: string;
publicMode?: boolean;
trustedProxies?: string[];
deps: ReturnType<typeof createDefaultDeps>;
}) {
return agentCommandFromIngress(
{
message: params.message,
images: params.images.length > 0 ? params.images : undefined,
clientTools: params.clientTools.length > 0 ? params.clientTools : undefined,
extraSystemPrompt: params.extraSystemPrompt || undefined,
streamParams: params.streamParams ?? undefined,
sessionKey: params.sessionKey,
runId: params.runId,
deliver: false,
messageChannel: params.messageChannel,
bestEffortDeliver: false,
// HTTP API callers are authenticated operator clients for this gateway context.
senderIsOwner: true,
allowModelOverride: true,
},
{ ...buildResponsesAgentCommandInput(params), allowModelOverride: true },
defaultRuntime,
params.deps,
);
}
export const __testOnlyOpenResponsesHttp = {
buildResponsesAgentCommandInput,
resolveResponsesLimits,
};
export async function handleOpenResponsesHttpRequest(
req: IncomingMessage,
res: ServerResponse,
@ -445,6 +475,9 @@ export async function handleOpenResponsesHttpRequest(
defaultMessageChannel: "webchat",
useMessageChannelHeader: false,
});
const runtimeConfig = opts.runtimeConfig ?? {};
const effectiveAgentId = resolveSessionAgentId({ sessionKey, config: runtimeConfig });
const publicMode = resolveAgentPublicMode(runtimeConfig, effectiveAgentId);
// Build prompt from input
const prompt = buildAgentPrompt(payload.input);
@ -483,6 +516,7 @@ export async function handleOpenResponsesHttpRequest(
if (!stream) {
try {
const result = await runResponsesAgentCommand({
req,
message: prompt.message,
images,
clientTools: resolvedClientTools,
@ -491,6 +525,8 @@ export async function handleOpenResponsesHttpRequest(
sessionKey,
runId: responseId,
messageChannel,
publicMode,
trustedProxies: opts.trustedProxies,
deps,
});
@ -710,6 +746,7 @@ export async function handleOpenResponsesHttpRequest(
void (async () => {
try {
const result = await runResponsesAgentCommand({
req,
message: prompt.message,
images,
clientTools: resolvedClientTools,
@ -718,6 +755,7 @@ export async function handleOpenResponsesHttpRequest(
sessionKey,
runId: responseId,
messageChannel,
publicMode,
deps,
});

View File

@ -834,6 +834,7 @@ export function createGatewayHttpServer(opts: {
handleOpenResponsesHttpRequest(req, res, {
auth: resolvedAuth,
config: openResponsesConfig,
runtimeConfig: configSnapshot,
trustedProxies,
allowRealIpFallback,
rateLimiter,
@ -847,6 +848,7 @@ export function createGatewayHttpServer(opts: {
handleOpenAiHttpRequest(req, res, {
auth: resolvedAuth,
config: openAiChatCompletionsConfig,
runtimeConfig: configSnapshot,
trustedProxies,
allowRealIpFallback,
rateLimiter,