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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ openapi:
output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \
python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --swag-roots "$$output/roots.go"; \
$(SWAG) init \
-g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,./internal/modelprovider,$$output" \
-g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,./internal/modelprovider,./internal/sandboxbootstrap,$$output" \
--output "$$output" \
--outputTypes yaml --parseInternal; \
--outputTypes yaml --parseInternal --parseFuncBody; \
python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \
go run ./scripts/openapi-split $(OPENAPI_FLAGS) "$$output/swagger.yaml" "$$output/extensions.json" "$$output/core.json" contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml; \
python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --extensions "$$output/extensions.json" --core "$$output/core.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/transport/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestBootstrapNonSuccessSurfacesBody(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"bad_credential","detail":"wrong key"}`))
_, _ = w.Write([]byte(`{"error":{"code":"bad_credential","message":"wrong key","type":"invalid_request_error","param":null}}`))
}))
defer srv.Close()

Expand Down
5 changes: 2 additions & 3 deletions apps/daemon/internal/transport/ws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import (
"testing"
"time"

"github.com/gorilla/websocket"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
"github.com/gorilla/websocket"
)

// fakeGateway is a minimal stand-in for the server-side
Expand Down Expand Up @@ -364,7 +363,7 @@ func TestDialMarksOperatorFixableUpgradeRejectionsAsPermanent(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tc.code)
_, _ = w.Write([]byte(`{"error":"x","detail":"y"}`))
_, _ = w.Write([]byte(`{"error":{"code":"x","message":"y","type":"invalid_request_error","param":null}}`))
}))
defer srv.Close()
_, err := transport.Dial(context.Background(), transport.DialOptions{
Expand Down
9 changes: 3 additions & 6 deletions apps/daemon/internal/wireconformance/wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package wireconformance

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
Expand All @@ -15,13 +14,13 @@ import (
"testing"
"time"

"github.com/gorilla/websocket"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest"
"github.com/gorilla/websocket"
)

const wait = 3 * time.Second
Expand All @@ -48,9 +47,7 @@ func newCorePeer(t *testing.T) *corePeer {
return
}
if query.Get("version") != proto.Version {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(prototest.IncompatibleVersionStatus)
_ = json.NewEncoder(w).Encode(map[string]string{"error": prototest.IncompatibleVersionCode})
v1.WriteHTTPError(w, prototest.IncompatibleVersionStatus, prototest.IncompatibleVersionCode, "incompatible Runtime version")
return
}
ws, err := upgrader.Upgrade(w, r, nil)
Expand Down
16 changes: 11 additions & 5 deletions contracts/agents-api/machine-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Machine connection API"
---

Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons, the Sandbox I/O service and the self-hosted installer. Each route accepts only the credential listed for it, never the Core key or a Project API key, and a console sign-in grants nothing here. The reverse proxy sends `/api/v1` directly to Core; Web never serves these routes.
Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons, the Sandbox I/O service and the self-hosted installer. Each route accepts only the credential listed for it, never the Core key or a Project API key, and a console sign-in grants nothing here. The public origin forwards `/api/v1` to Core, either directly or through Web's unchanged HTTP and WebSocket proxy. Web adds no console authority to machine requests.

## Routes

Expand All @@ -12,7 +12,7 @@ Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons, the Sandbox
| `POST sandbox-node/enroll` | Node installer | Enrollment token | [Enroll a node](#enroll-a-node) |
| `GET sandbox-node/identity?node_id=` | Node | Node credential | [Recover a node's identity](#recover-a-nodes-identity) |
| WebSocket `GET sandbox-node/connect?node_id=` | Node | Node credential | [Node generation protocol](./node-generation-protocol.md) |
| `GET agent-daemon/install/{version}/…` | Self-hosted installer | None | [Installation grant](./environment-executor-credentials.md#installation-grant) |
| `GET` / `HEAD agent-daemon/install/{version}/…` | Self-hosted installer | None | [Installation grant](./environment-executor-credentials.md#installation-grant) |
| `POST agent-daemon/installation`, `POST agent-daemon/installation/claim` | Self-hosted installer | Installation grant | [Installation grant](./environment-executor-credentials.md#installation-grant) |
| `POST agent-daemon/enroll` | Self-hosted daemon | Executor credential | [Enroll a self-hosted daemon](#enroll-a-self-hosted-daemon) |
| `GET agent-daemon/connection?environment_id=` | Self-hosted installer | Executor credential | [Private connection confirmation](./environment-executor-credentials.md#private-connection-confirmation) |
Expand All @@ -22,7 +22,13 @@ Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons, the Sandbox

Every credential travels in an `Authorization: Bearer` header, except on `sandbox-link`, where each peer sends it in its Link Hello after the upgrade. No credential travels in a URL. Core derives the [Link URL](../../docs/configuration.md#changing-the-public-url) from `OAC_PUBLIC_URL`.

The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only the sandbox-node configuration, enroll and identity routes, the two installation routes and the `sandbox-link` upgrade, whose messages the Sandbox link protocol defines. The `sandbox-node/connect` and `agent-daemon/ws` WebSockets and the daemon bootstrap, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition.
The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes every machine HTTP operation, including public downloads and WebSocket handshakes. WebSocket messages remain owned by their linked wire protocols.

### HTTP errors and methods

Every HTTP error uses `{"error":{"message":"…","type":"invalid_request_error","code":null,"param":null}}`. `code` carries a route's defined reason when present; `param` identifies a rejected field when present. A 409 has type `conflict_error`, and a 5xx has type `server_error`. Handshake failures use the same envelope before upgrade; errors after upgrade belong to the wire protocol. Consumers use HTTP status for retry and permanent-rejection decisions and may display `error.message`.

`HEAD` never opens a connection or queries a Runtime: daemon WebSocket, Link and connection observation routes reject it with 405; the node connection rejects standard HTTP methods other than GET with 503. Unsupported extension methods such as `PROPFIND` are rejected by the shared router with 405 before authentication or upgrade. Node configuration and identity reads support HEAD with the same credential checks as GET. Installer downloads support GET and HEAD, including archive conditional and range responses; download errors also use the shared envelope. Enrollment accepts POST only. Other rejected methods use the shared error envelope. The bare `/api/v1/agent-daemon` and `/api/v1/agent-daemon/install` prefixes redirect with 301 to their trailing-slash forms, preserving their queries. Unknown machine routes return 404 in the shared envelope.

## Credentials

Expand Down Expand Up @@ -88,7 +94,7 @@ The credential is checked before any deployment state, so a rejected credential,

`POST /api/v1/agent-daemon/bootstrap` with the agent-host credential and `{"device_id": "…"}` returns `device_id`, `workspace_id` (an empty string for the deployment-scoped host), `ws_url` (derived from `OAC_PUBLIC_URL`, never from request headers), `heartbeat_seconds` and `protocol_version`. The daemon then dials `ws_url` as the [Core–Runtime protocol](../../docs/runtime-protocol.md#ownership-and-connection) describes.

The bootstrap and WebSocket routes share one error body, `{"error": code, "detail": text}`: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version.
The bootstrap and WebSocket routes report these `error.code` values: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version.

### Enroll a self-hosted daemon

Expand All @@ -99,6 +105,6 @@ The bootstrap and WebSocket routes share one error body, `{"error": code, "detai
| 400 | A malformed body or any query |
| 401 | An invalid, revoked or foreign credential, a deleted Session, or an Environment without current executor authority |
| 409 | Another executor key already enrolled the Environment |
| 503 | `{"error": "no_sandbox_link", "detail": "a self_hosted sandbox needs an https public URL"}`, before the credential is checked, when the [public URL](../../docs/configuration.md#changing-the-public-url) is not https; otherwise storage is unavailable |
| 503 | `error.code: "no_sandbox_link"` and `error.message: "a self_hosted sandbox needs an https public URL"`, after header/body validation but before executor authority is checked, when the [public URL](../../docs/configuration.md#changing-the-public-url) is not https; otherwise storage is unavailable |

Enrollment creates no managed allocation, binds no Session and grants no Session API access. Core binds the Session to the deployment's agent host while the machine serves the resource ([Session assignments](../../docs/runtime-protocol.md#session-assignments)). The relay rechecks the credential's authority on every Serve and Open, so rotation, revocation and Session deletion end further use. The [self-hosted guide](../../docs/getting-started/self-hosted.md) gives the operator steps, and the [executor credential contract](./environment-executor-credentials.md#revoked-or-rotated-credential) describes how the daemon handles a permanent rejection.
Loading
Loading