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
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Run the `engine` and `execution` tests for omission, policy, combination and err

A supplied `model` must be a nonempty string, and an explicit `model_provider` requires it. The native-owned connection path may omit both; explicit null is invalid. An explicitly empty declaration accepts no provider or nonempty native parameters and advertises no provider support. Unknown protocol formats and duplicate protocol declarations fail at registration.

The declaration's ordered `protocols` list is the only source of accepted protocols and the default (the first entry); it also feeds Core's configuration-support descriptor, and Core and Runtime reject unsupported combinations through it. Adapters connect directly through native configuration; they never introduce a model API proxy or protocol converter, a second model capability registry, or capabilities inferred from model names. Claude's private bridge receives compiled native options and performs structural checks only, not a second copy of the declaration's rules.
The declaration's ordered `protocols` list is the only source of accepted protocols and the default (the first entry); it also feeds Core's configuration-support descriptor, and Core and Runtime reject unsupported combinations through it. Adapters connect through native configuration and the [credential gateway](model-execution.md#credential-gateway); they never introduce their own model API proxy or protocol converter, a second model capability registry, or capabilities inferred from model names. Claude's private bridge receives compiled native options and performs structural checks only, not a second copy of the declaration's rules.

## Qualify the adapter

Expand Down
19 changes: 15 additions & 4 deletions contracts/agents-api/model-execution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Model execution

Each Session runs one Harness with one model provider. Core selects them through three Core extensions that the pinned upstream protocol does not define: `x_agents_core.harness` chooses the Harness, `x_agents_core.model_provider` supplies the endpoint and key, and `x_agents_core.harness_config` carries native model parameters. Core has no provider catalog, model alias resolution or product permission model; besides Session and saved-Agent bundles, the only stored bundle is one [deployment default](#deployment-defaults) per Harness. This document is the Harness–model provider protocol: [`internal/modelprovider/config.go`](../../internal/modelprovider/config.go) validates the frozen provider connection, and each Harness declares its protocols and native parameters through [`internal/harnessconfig/harness.go`](../../internal/harnessconfig/harness.go).
Each Session runs one Harness with one model provider. Core selects them through three Core extensions that the pinned upstream protocol does not define: `x_agents_core.harness` chooses the Harness, `x_agents_core.model_provider` supplies the endpoint and key, and `x_agents_core.harness_config` carries native model parameters. Core has no provider catalog, model alias resolution or product permission model; besides Session and saved-Agent bundles, the only stored bundle is one [deployment default](#deployment-defaults) per Harness. This document is the Harness–model provider protocol: [`internal/modelprovider/config.go`](../../internal/modelprovider/config.go) validates the frozen provider connection and declares what the [credential gateway](#credential-gateway) relays, and each Harness declares its protocols and native parameters through [`internal/harnessconfig/harness.go`](../../internal/harnessconfig/harness.go).

## Harness selection

Expand Down Expand Up @@ -32,7 +32,7 @@ Provider precedence is: a complete Session bundle, then a complete saved bundle,
| Claude SDK | `anthropic` |
| MiniMax Code | `anthropic`, `responses`, `chat_completions` |

MiniMax Code requires positive context and output limits. Core validates the resolved combination before writing the Session. Core and Runtime read the same ordered `protocols` declaration in `internal/harnessconfig`. There is no model API proxy, passthrough gateway or cross-protocol conversion, including inside a Harness. Unsupported saved configurations and Session snapshots fail when used; they are never rewritten, aliased or migrated.
MiniMax Code requires positive context and output limits. Core validates the resolved combination before writing the Session. Core and Runtime read the same ordered `protocols` declaration in `internal/harnessconfig`. Nothing converts between protocols, including inside a Harness. Unsupported saved configurations and Session snapshots fail when used; they are never rewritten, aliased or migrated.

Which sources apply depends on who owns the compute that receives the key:

Expand All @@ -42,7 +42,7 @@ Which sources apply depends on who owns the compute that receives the key:
| `self_hosted` | Accepted | Never applied | 400 `model_provider_required` |
| `none` | Rejected with 400 | Applied when configured | Accepted; the device's own environment supplies the model |

The deployment default holds the operator's key, so it stays on operator compute: Core-managed sandboxes and operator-registered `none` devices. A `self_hosted` executor belongs to the application, which supplies its own bundle. Hosted and self-hosted Runtimes carry no model configuration of their own, so a Session there without a bundle is rejected before any write, with param `x_agents_core.model_provider` and a message saying what to configure.
The deployment default holds the operator's key, so it applies only to operator-run Environments: Core-managed sandboxes and operator-registered `none` devices. A `self_hosted` executor belongs to the application, which supplies its own bundle. Hosted and self-hosted Runtimes carry no model configuration of their own, so a Session there without a bundle is rejected before any write, with param `x_agents_core.model_provider` and a message saying what to configure.

| Operation | Omitted | Explicit null |
| --- | --- | --- |
Expand Down Expand Up @@ -88,7 +88,18 @@ Unsupported protocol, Harness or Environment combinations are rejected before th

The resolved provider configuration is frozen and encrypted in the Session creation transaction, with its own encryption purpose and Project and Session binding. Creation retries include it in their request hash, so a changed key or endpoint under the same Idempotency-Key conflicts; a key enters any stored hash only as a fingerprint keyed by the deployment credential key. No public Session, Agent, Environment, event or ordinary configuration contains the key. The top-level extension is write-only and cannot be updated.

At dispatch, Core sends the snapshot as one confidential provider bundle over the daemon connection bound to the Session, and the adapter applies it natively and connects directly to the provider. Core never falls back to other credentials when a snapshot is missing or cannot be decrypted. For `self_hosted`, the receiving daemon is the executor enrolled for the Session's own Environment with a current executor credential of the Session creator's principal; rotation or revocation closes the socket before further dispatch. The executor host stores the bundle in its native Harness home, as hosted Runtimes do. Native tools run with the starting account's permissions and can read what that account can read, and revocation does not erase a bundle already delivered.
At dispatch, Core sends the snapshot as one confidential provider bundle over the daemon connection bound to the Session, and the adapter points the Harness at the [credential gateway](#credential-gateway) through native configuration. Core never falls back to other credentials when a snapshot is missing or cannot be decrypted. For `self_hosted`, the receiving daemon is the executor enrolled for the Session's own Environment with a current executor credential of the Session creator's principal; rotation or revocation closes the socket before further dispatch. The gateway holds the key in memory for the Session, and the key never enters the Harness's environment, configuration or home, or the sandbox.

## Credential gateway

The Harness reaches its frozen upstream through a Session-local credential gateway on the agent host. The Harness's native base URL points at the gateway, and the Harness receives only a non-secret placeholder credential, never the key.

- The gateway relays only the declared native routes of the provider's protocol, with the request and response unchanged. An undeclared path or method, or an undeclared WebSocket upgrade, is rejected and never reaches the upstream.
- It removes every value of each stripped header, matching names case-insensitively, then injects the upstream credential in the protocol's declared header.
- It never follows a redirect with the credential.
- It never converts between protocols.

[`internal/modelprovider/config.go`](../../internal/modelprovider/config.go) declares each protocol's routes and credential header, the stripped headers and the placeholder. Its `LookupRoute` matches a request against the routes, and `UpstreamPath` joins a matched route to the upstream base URL. A Harness that calls a route the table does not declare needs a protocol change, not a gateway exception.

## Native model parameters

Expand Down
2 changes: 1 addition & 1 deletion docs/api/public-agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Any other member is rejected with 400. `api_key` is write-only: reads return `ap
The harness is the agent program that runs a Session: Codex (`codex`), Claude Code (`claude_sdk`) or MiniMax Code (`mcode`). Set `x_agents_core.harness` on the Agent or the inline `agent`; without it, the installation's default harness applies ([`core.default_harness`](../configuration.md#settings), Codex unless the operator changed it).

- **Model.** `model` is the provider's exact model ID. An inline Agent on an `openai_hosted` or `none` Session may omit it to use the default model configuration of its harness. A saved Agent always needs one.
- **Provider.** The harness calls your provider directly, with one of the harness's native protocols; there is no conversion, and a mismatch is rejected when the Session is created. [Model execution](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) lists each harness's protocols and which provider a Session uses on each Environment type. A Session freezes its provider at creation.
- **Provider.** The harness calls your provider with one of the harness's native protocols, through a [credential gateway](../../contracts/agents-api/model-execution.md#credential-gateway) that keeps your key out of the harness; there is no conversion, and a mismatch is rejected when the Session is created. [Model execution](../../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) lists each harness's protocols and which provider a Session uses on each Environment type. A Session freezes its provider at creation.
- **Native parameters.** `harness_config` carries the harness's own model settings; see [native model parameters](../../contracts/agents-api/model-execution.md#native-model-parameters).

Not every combination of harness, placement and operation is supported; the [Harness capabilities](../../contracts/agents-api/harness-capabilities.md) lists them.
Expand Down
9 changes: 5 additions & 4 deletions internal/harnessconfig/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
// Core freezes the model configuration per Session. Its meaning and explicit
// settings must hold for the first Turn, later Turns, native retries, tool
// continuations and recovery. A Runtime that cannot preserve a frozen snapshot
// rejects it without rewriting, migration or aliases. Native connection setup is
// direct: no model API proxy, passthrough gateway or protocol conversion, including
// inside an adapter. Protocol acceptance does not qualify model capabilities;
// operation and input requirements must still be checked before native submission.
// rejects it without rewriting, migration or aliases. Native connection setup
// goes through the credential gateway that internal/modelprovider declares, with
// no protocol conversion, including inside an adapter. Protocol acceptance does
// not qualify model capabilities; operation and input requirements must still be
// checked before native submission.
package harnessconfig

import (
Expand Down
114 changes: 113 additions & 1 deletion internal/modelprovider/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Package modelprovider validates the frozen upstream connection supplied to a native Harness.
// Package modelprovider validates the frozen upstream connection supplied to a
// native Harness and declares the native routes and credential header that the
// Session's credential gateway relays for each protocol.
package modelprovider

import (
Expand All @@ -7,6 +9,7 @@ import (
"errors"
"net"
"net/url"
"slices"
"strings"
)

Expand Down Expand Up @@ -78,3 +81,112 @@ func (p Provider) Validate() error {
}
return nil
}

// Placeholder is the credential a Harness receives instead of the upstream key.
// It is not secret and authorizes nothing outside the Session's gateway listener.
const Placeholder = "oac-gateway-placeholder"

// Route is one native HTTP route of a protocol. Path is relative to the
// upstream base URL; UpstreamPath joins the two, and the gateway relays the
// request's query unchanged.
type Route struct {
Method string
Path string
// WebSocket allows an upgrade on this route; otherwise the gateway
// rejects a request that asks for one.
WebSocket bool
}

// UpstreamPath is the escaped upstream path for a route: the base URL's escaped
// path with every trailing "/" removed, followed by the route's Path.
func UpstreamPath(baseEscapedPath, routePath string) string {
return strings.TrimRight(baseEscapedPath, "/") + routePath
}

// Credential is the upstream credential header the gateway injects. Its value
// is Prefix followed by the key.
type Credential struct {
Header string
Prefix string
}

func (c Credential) Value(key string) string { return c.Prefix + key }

// StrippedHeaders lists, in canonical MIME form, every inbound credential
// header that a pinned Harness or its SDK can send. The gateway matches the
// names case-insensitively and removes every value of each before it injects
// the upstream credential.
var StrippedHeaders = []string{
"Authorization",
"Proxy-Authorization",
"Cookie",
"X-Api-Key",
"Api-Key",
"X-Openai-Actor-Authorization",
"Cf-Aig-Authorization",
"X-Amz-Security-Token",
}

// surface is the declared native API of one protocol: the routes the pinned
// Harnesses call and the credential form they send upstream.
type surface struct {
routes []Route
credential Credential
}

var surfaces = map[Protocol]surface{
Anthropic: {
routes: []Route{
{Method: "POST", Path: "/v1/messages"},
{Method: "POST", Path: "/v1/messages/count_tokens"},
},
credential: Credential{Header: "X-Api-Key"},
},
Responses: {
routes: []Route{{Method: "POST", Path: "/responses"}},
credential: Credential{Header: "Authorization", Prefix: "Bearer "},
},
ChatCompletions: {
routes: []Route{{Method: "POST", Path: "/chat/completions"}},
credential: Credential{Header: "Authorization", Prefix: "Bearer "},
},
}

var (
ErrRouteNotFound = errors.New("model route is not declared")
ErrMethodNotAllowed = errors.New("model route does not allow this method")
)

// Routes returns the declared routes of a protocol.
func Routes(p Protocol) []Route { return slices.Clone(surfaces[p].routes) }

// UpstreamCredential returns the credential header the gateway injects for p.
func UpstreamCredential(p Protocol) (Credential, error) {
s, ok := surfaces[p]
if !ok {
return Credential{}, ErrConfiguration
}
return s.credential, nil
}

// LookupRoute matches a request against the declared routes of p. The path is
// the request's escaped path relative to the base URL and matches exactly,
// with no normalization; the method matches exactly. A declared path with
// another method is ErrMethodNotAllowed; any other path is ErrRouteNotFound.
func LookupRoute(p Protocol, method, path string) (Route, error) {
s, ok := surfaces[p]
if !ok {
return Route{}, ErrConfiguration
}
err := ErrRouteNotFound
for _, route := range s.routes {
if route.Path != path {
continue
}
if route.Method == method {
return route, nil
}
err = ErrMethodNotAllowed
}
return Route{}, err
}
84 changes: 84 additions & 0 deletions internal/modelprovider/routes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package modelprovider

import (
"errors"
"net/http"
"slices"
"testing"
)

func TestLookupRouteMatchesOnlyDeclaredRoutes(t *testing.T) {
for _, header := range StrippedHeaders {
if http.CanonicalHeaderKey(header) != header {
t.Fatalf("stripped header %q is not canonical", header)
}
}
for _, protocol := range []Protocol{Anthropic, Responses, ChatCompletions} {
routes := Routes(protocol)
if len(routes) == 0 {
t.Fatalf("%s declares no routes", protocol)
}
credential, err := UpstreamCredential(protocol)
if err != nil {
t.Fatalf("%s declares no credential: %v", protocol, err)
}
if !slices.Contains(StrippedHeaders, credential.Header) {
t.Fatalf("%s credential header %s is not stripped", protocol, credential.Header)
}
for _, route := range routes {
if got, err := LookupRoute(protocol, route.Method, route.Path); err != nil || got != route {
t.Fatalf("%s %s %s did not match: %v", protocol, route.Method, route.Path, err)
}
for _, path := range []string{route.Path + "/", route.Path + "x", "/v1/../" + route.Path[1:], "/" + route.Path} {
if _, err := LookupRoute(protocol, route.Method, path); !errors.Is(err, ErrRouteNotFound) {
t.Fatalf("%s %s matched: %v", protocol, path, err)
}
}
if _, err := LookupRoute(protocol, "DELETE", route.Path); !errors.Is(err, ErrMethodNotAllowed) {
t.Fatalf("%s DELETE %s: %v", protocol, route.Path, err)
}
}
}
for _, miss := range []struct {
protocol Protocol
method, path string
want error
}{
{Anthropic, "GET", "/v1/models", ErrRouteNotFound},
{Anthropic, "POST", "/v1/files", ErrRouteNotFound},
{Anthropic, "post", "/v1/messages", ErrMethodNotAllowed},
{Anthropic, "POST", "/responses", ErrRouteNotFound},
{Responses, "POST", "/v1/messages", ErrRouteNotFound},
{ChatCompletions, "POST", "/responses", ErrRouteNotFound},
{"openai", "POST", "/responses", ErrConfiguration},
} {
if _, err := LookupRoute(miss.protocol, miss.method, miss.path); !errors.Is(err, miss.want) {
t.Fatalf("%s %s %s = %v, want %v", miss.protocol, miss.method, miss.path, err, miss.want)
}
}
}

func TestPlaceholderPassesProviderValidation(t *testing.T) {
for _, protocol := range []Protocol{Anthropic, Responses, ChatCompletions} {
gateway := Provider{Protocol: protocol, BaseURL: "http://127.0.0.1:41000", APIKey: Placeholder, ContextWindow: 64000, MaxOutputTokens: 4096}
if err := gateway.Validate(); err != nil {
t.Fatalf("%s rejected the placeholder: %v", protocol, err)
}
}
}

func TestUpstreamPathJoinsBaseAndRoute(t *testing.T) {
for _, join := range []struct{ base, route, want string }{
{"", "/responses", "/responses"},
{"/", "/responses", "/responses"},
{"/v1", "/responses", "/v1/responses"},
{"/v1/", "/responses", "/v1/responses"},
{"/v1//", "/responses", "/v1/responses"},
{"/anthropic", "/v1/messages", "/anthropic/v1/messages"},
{"/a%2Fb/v1", "/chat/completions", "/a%2Fb/v1/chat/completions"},
} {
if got := UpstreamPath(join.base, join.route); got != join.want {
t.Fatalf("UpstreamPath(%q, %q) = %q, want %q", join.base, join.route, got, join.want)
}
}
}
Loading