Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1849,13 +1849,14 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
timestamps and metadata, separately from saved configuration and Session state.
Resolve known defaults and validate supported schema before writing. Preserve
model/name/instructions verbatim, nullable fields and structured JSON numbers.
Stored reasoning/service tiers, enabled multi-agent settings, JSON Schema output
and deferred/tool-search/programmatic tools do not imply execution support.
Reuse function wire validation, keeping Session execution restrictions separate.
Model-derived reasoning effort is unresolved when omitted; do not infer it from
the selected harness. Omitted/null service tier currently uses `auto`; complete
upstream default/error/retry conformance and remaining MCP/web-search variants
remain gaps. Unknown/unsupported variants fail explicitly. No product lookup is permitted.
Stored reasoning/service tiers, enabled multi-agent settings, JSON Schema output,
enabled web-search modes and deferred/tool-search/programmatic tools do not imply
execution support. Reuse function wire validation, keeping Session execution
restrictions separate. Model-derived reasoning effort is unresolved when omitted;
do not infer it from the selected harness. Omitted/null service tier currently
uses `auto`; complete upstream default/error/retry conformance and remaining MCP
variants remain gaps. Unknown/unsupported variants fail explicitly. No product
lookup is permitted.
- Service-origin public HTTP MCP uses the native harness client and tool loop on
trusted service-owned `environment:none` compute, with Codex or Claude SDK.
The V1 colocated `self_hosted` profile rejects it: user-owned compute cannot be
Expand Down Expand Up @@ -2211,7 +2212,8 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
not remove unrelated native utilities or claim enabled programmatic support.
Explicit `web_search.mode=disabled` reuses the existing disabled search control.
Search remains off when omitted. Optional search settings are resource data and
do not cause execution while disabled. Enabled search remains unqualified here.
do not cause execution while disabled. Saved Agents keep every pinned search mode;
enabled search remains unqualified and rejects at Session admission.
- `web_search_control` advertises the Codex adapter's explicit `web_search` option
(`disabled`, `cached`, or `live`). Agents API requires this capability before Codex dispatch;
the typed execution controls force search off on new and resumed Turns. Native configuration translation stays in the
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/features/agents/agent-form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,20 @@ describe("Agent form contract", () => {
})).toEqual({ toolsError: "At most 64 Functions can be configured." });
});

it.each([
["live", "Enabled web_search is saved-only; Core does not run enabled search in a Session"],
["cached", "Enabled web_search is saved-only; Core does not run enabled search in a Session"],
["disabled", "Disabled web_search is read-only in Web; Sessions keep search off"],
])("labels saved %s web_search as a read-only tool without misstating Core admission", (mode, label) => {
const agent = {
id: "agent_1", object: "agent" as const, model: "provider/model", name: null, instructions: null, metadata: {},
multi_agent: { enabled: false, max_concurrent_subagents: null }, reasoning: {}, service_tier: "auto" as const,
text: { format: { type: "text" as const }, verbosity: "medium" as const }, created_at: 1, updated_at: 1,
tools: [{ type: "web_search", mode, context_size: "medium", allowed_domains: [], location: null }],
};
expect(valuesFromAgent(agent, null).tools[0]).toMatchObject({ kind: "read-only", label });
});

it("omits read-only saved tools for unrelated updates and rejects deliberate Tool changes", () => {
const opaque = { type: "mcp", credential_id: "vault-bound", headers: { Authorization: "not-rendered" } };
const agent = {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/features/agents/agent-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ function readOnlyToolLabel(value: unknown): string {
&& hasOnlyKeys(value, ["type", "enabled"])
&& (value.enabled === undefined || typeof value.enabled === "boolean")
) return "programmatic_tool_calling is saved-only and cannot run in a Session";
if (
value.type === "web_search"
&& hasOnlyKeys(value, ["type", "mode", "context_size", "allowed_domains", "location"])
) {
return value.mode === "disabled"
? "Disabled web_search is read-only in Web; Sessions keep search off"
: "Enabled web_search is saved-only; Core does not run enabled search in a Session";
}
if (value.type === "function" && value.defer_loading === true) return "Deferred Function is saved-only";
if (value.type === "mcp" && value.credential_id != null) return "Credentialed MCP is unresolved or URL-mismatched in the current Vault catalog";
if (value.type === "function") return "Unsupported Function tool definition";
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/features/agents/session-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ describe("known Session admission blockers", () => {
[{ text: { format: { type: "json_schema" as const, schema: { type: "object" } }, verbosity: "medium" as const } }, "text format must be text"],
[{ multi_agent: { enabled: true, max_concurrent_subagents: 2 } }, "multi-agent execution is not supported"],
[{ tools: [{ type: "tool_search" }] }, "tool_search is saved-only"],
[{ tools: [{ type: "web_search", mode: "live", context_size: "medium", allowed_domains: null, location: null }] }, "web_search mode must be disabled because Core does not run enabled search"],
[{ tools: [{ type: "web_search", mode: "cached" }] }, "web_search mode must be disabled because Core does not run enabled search"],
[{ tools: [{ type: "web_search" }] }, "web_search mode must be disabled because Core does not run enabled search"],
[{ tools: [{ type: "web_search", mode: "disabled", location: { extra: "x" } }] }, "the saved web_search tool is incomplete or malformed"],
[{ tools: [{ type: "web_search", mode: "disabled" }, { type: "web_search", mode: "disabled" }] }, "at most one web_search tool can execute"],
[{ tools: [{ type: "function", name: "later", description: "", parameters: {}, defer_loading: true }] }, "deferred functions are saved-only"],
[{ tools: [{ type: "function", name: " ", description: "", parameters: {}, defer_loading: false }] }, "function names must be non-empty and at most 512 bytes"],
[{ tools: [{ type: "function", name: "\u0085", description: "", parameters: {}, defer_loading: false }] }, "function names must be non-empty and at most 512 bytes"],
Expand Down Expand Up @@ -118,6 +123,11 @@ describe("known Session admission blockers", () => {
expect(knownSessionAdmissionBlockers(agent)).toContain(message);
});

it("admits saved disabled web_search, which Core runs with search off", () => {
const tools = [{ type: "web_search", mode: "disabled", context_size: "medium", allowed_domains: [], location: { city: "Paris", country: null, region: null, timezone: null } }];
expect(knownSessionAdmissionBlocker({ ...executableAgent, tools })).toBeNull();
});

it("does not turn conditional Codex verbosity support into a deterministic blocker", () => {
const agent = { ...executableAgent, text: { ...executableAgent.text, verbosity: "high" as const } };
expect(knownSessionAdmissionBlocker(agent)).toBeNull();
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/features/agents/session-admission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ function isCanonicalExecutionFunction(tool: Record<string, unknown>): boolean {
&& (tool.defer_loading === undefined || typeof tool.defer_loading === "boolean");
}

// Mirrors Core's saved web_search projection; Core runs only mode "disabled".
function isCanonicalWebSearch(tool: Record<string, unknown>): boolean {
const domains = tool.allowed_domains;
const location = tool.location;
return hasOnlyKeys(tool, ["type", "mode", "context_size", "allowed_domains", "location"])
&& (tool.mode == null || ["disabled", "cached", "live"].includes(String(tool.mode)))
&& (tool.context_size == null || ["low", "medium", "high"].includes(String(tool.context_size)))
&& (domains == null || Array.isArray(domains) && domains.every((domain) => typeof domain === "string"))
&& (location == null || isRecord(location)
&& hasOnlyKeys(location, ["city", "country", "region", "timezone"])
&& Object.values(location).every((value) => value === null || typeof value === "string"));
}

function isCanonicalExecutionMcp(tool: Record<string, unknown>): boolean {
const transport = tool.transport;
const allowedTools = tool.allowed_tools;
Expand Down Expand Up @@ -130,6 +143,7 @@ export function knownSessionAdmissionBlockers(agent: SavedAgent): string[] {
const functionNames = new Set<string>();
const mcpLabels = new Set<string>();
let functionCount = 0;
let searchCount = 0;
for (const rawTool of agent.tools) {
if (!rawTool || typeof rawTool !== "object" || Array.isArray(rawTool)) {
blockers.push("the saved tool configuration is not executable");
Expand Down Expand Up @@ -158,6 +172,11 @@ export function knownSessionAdmissionBlockers(agent: SavedAgent): string[] {
mcpLabels.add(tool.server_label);
}
break;
case "web_search":
searchCount += 1;
if (!isCanonicalWebSearch(tool)) blockers.push("the saved web_search tool is incomplete or malformed");
else if (tool.mode !== "disabled") blockers.push("web_search mode must be disabled because Core does not run enabled search");
break;
case "tool_search":
case "programmatic_tool_calling":
blockers.push(`${String(tool.type)} is saved-only`);
Expand All @@ -167,6 +186,7 @@ export function knownSessionAdmissionBlockers(agent: SavedAgent): string[] {
}
}
if (functionCount > 64) blockers.push("at most 64 function tools can execute");
if (searchCount > 1) blockers.push("at most one web_search tool can execute");
return [...new Set(blockers)];
}

Expand Down
11 changes: 6 additions & 5 deletions contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,11 @@ upgrade the protocol.
- [Explicit disabled tools](tool-policy.md) can be saved, used inline or resolved from saved Agents:
`web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`. Search
responses include `context_size=medium` for omitted/null size, nullable domains
and location; an empty domain list stays empty. Only explicit disabled mode is
qualified; omitted/null mode and enabled search remain gaps. Sessions reject
enabled programmatic execution, including the default true on a supplied PTC
declaration. An omitted PTC declaration preserves native behavior: this is an
and location; an empty domain list stays empty. Saved Agents keep every pinned
search mode, saving omitted/null mode as `live`; only explicit disabled mode is
qualified, so Session admission rejects enabled search unless the Session
replaces the saved tools. Sessions reject enabled programmatic execution,
including the default true on a supplied PTC declaration. An omitted PTC declaration preserves native behavior: this is an
approved difference from the official default-on behavior, not full compatibility.
Core carries the frozen disabled intent through the common Runtime contract;
native translation and inventory restrictions stay in adapters. Codex checks
Expand Down Expand Up @@ -355,7 +356,7 @@ upgrade the protocol.
rechecked before dispatch-only decryption; authenticated execution requires the
separate bearer capability and never downgrades on failure. Exact URL/selection
timing, implicit response population and hosted errors remain local or unverified.
Other MCP variants and enabled web-search remain gaps, not changes to the pinned target
Other MCP variants and enabled web-search execution remain gaps, not changes to the pinned target
or claims of complete resource coverage.

- Use `/agents/sessions` beneath the configured API base URL, bearer authentication
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/execution-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ and Environment Plugin MCP have separate inventories and qualification.
| Function text results and native application | Codex waits for a matching live root dynamic-tool completion; Claude waits for a matching live root native tool result. Text/error results, retry/conflict, cancellation and cold continuation: F1/F2. [Receipt contract](function-result-images.md). | Transport writes alone do not confirm application. Confirmation is not provider consumption, crash recovery or exactly-once external effects. No automatic result replay. |
| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. MiniMax functions and self-hosted image workflows are unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. |
| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading. [Contract](tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. |
| Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](tool-policy.md), [parser](../../services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects; a repeated `web_search` is a protocol error, while saved `live`/`cached`/omitted modes keep the local rejection (TV-05). Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. |
| Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](tool-policy.md), [parser](../../services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects at Session admission; saved Agents keep every pinned search mode as resource data (TV-05), and Sessions from such Agents reject unless they replace the tools. A repeated `web_search` is a protocol error. Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. |
| Agent service-origin MCP | Implemented Codex/Claude HTTP `none` profiles, anonymous/static bearer, scoped Vault selection and native `mcp_call` Items. [Configuration and qualification limits](../../services/agents-api/README.md#http-mcp-execution), [profiles](../../services/agents-api/internal/engine/profile.go). | This closure batch does not requalify MCP/provider combinations. MiniMax, self-hosted/hosted service-origin MCP, OAuth, nonempty inline headers/metadata and other transports/origins reject. Claude requires a static connected inventory; original MCP-envelope fidelity and continuing server health are unverified. |
| Environment Plugin MCP/native tools | Separate [Docker Plugin transport matrix](environment-templates.md#environment-origin-mcp-plugins) and [V1 deployment evidence](user-managed-runtime-v1.md). Native tools stay within the existing colocated Runtime. | Plugin MCP is not Agent service-origin MCP or a public function action. No new optional cross-product is qualified here. Native utility inventories need not be identical. |
| Required action: `function_call` | Session GET/list and Session SSE expose persisted `{arguments,call_id,name,turn_id,type}`. Actions remain pending until native application or cancellation/terminal settlement. [Projection](../../services/agents-api/internal/store/function_state.go), [confirmation](../../services/agents-api/internal/store/function_results.go); real handling F1/F2/M2/S1/S2/D1; explicit client disconnect/query/reconnect: F3. | Function-call history is not pending-state authority. Client reconnect does not replay events or redo external application effects. |
Expand Down
Loading
Loading