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
11 changes: 11 additions & 0 deletions contracts/agents-api/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package agentsapi embeds the generated OpenAPI documents of Core's three
// namespaces so Core can serve them at /docs.
package agentsapi

import "embed"

// OpenAPI holds openapi.yaml (/v1), core.openapi.yaml (/core/v1) and
// runtime.openapi.yaml (/api/v1), as generated by make openapi.
//
//go:embed openapi.yaml core.openapi.yaml runtime.openapi.yaml
var OpenAPI embed.FS
2 changes: 2 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ A credential used in another namespace gets 401: a Project API key on `/core/v1`

**Routing.** The reverse proxy sends `/v1` and `/api/v1` to Core and everything else to Web ([proxy setup](../getting-started/install-options.md#https-and-the-reverse-proxy)). Browsers reach `/core/v1` only through Web's console server, which adds the Core key after sign-in and answers 404 for `/v1` and `/api/v1` ([console server](../web/console-server.md)). Operator scripts call `/core/v1` on Core's loopback port ([script the Core API](../getting-started/operations.md#script-the-core-api)).

**API reference page.** Core serves a read-only Swagger UI of all three namespaces at `/docs`, and the generated documents it renders at `/docs/openapi.yaml`, `/docs/core.openapi.yaml` and `/docs/runtime.openapi.yaml`. These routes need no credential, and the page sends no API requests. The reverse proxy does not route `/docs` to Core, so open it on Core's own address: on the Core host, `http://127.0.0.1:<port>/docs`, where the port is [`ports.core`](../configuration.md#settings), 8091 by default. The browser loads Swagger UI from `unpkg.com`.

## Machine connection API

Nodes, Runtime daemons and the self-hosted installer call `/api/v1` with their own credentials. The [machine connection API](../../contracts/agents-api/machine-api.md) lists every route, caller and credential.
4 changes: 3 additions & 1 deletion docs/zh/api/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "API 命名空间和凭据"
source: docs/api/index.md
source_hash: 278dc9cde80bdc89641b98c7c8dd386c9405b386cd2ad82e7addaf73f60a6126
source_hash: d6483deff36f22b113e3d6df256c399238b8f4ffeeda889a1b0e3f0ddbdb6e67
---

Core 提供三个命名空间。每个命名空间都有一种调用方及其独立凭据,凭据只能在其所属命名空间中使用。
Expand All @@ -16,6 +16,8 @@ Core 提供三个命名空间。每个命名空间都有一种调用方及其独

**路由。** 反向代理将 `/v1` 和 `/api/v1` 发送到 Core,将其他所有请求发送到 Web([代理设置](../getting-started/install-options.md#https-and-the-reverse-proxy))。浏览器只能通过 Web 的控制台服务器访问 `/core/v1`;该服务器会在登录后添加 Core key,并对 `/v1` 和 `/api/v1` 返回 404([控制台服务器](../web/console-server.md))。操作员脚本通过 Core 的回环端口调用 `/core/v1`([编写 Core API 脚本](../getting-started/operations.md#script-the-core-api))。

**API 参考页面。** Core 在 `/docs` 提供三个命名空间的只读 Swagger UI,并在 `/docs/openapi.yaml`、`/docs/core.openapi.yaml` 和 `/docs/runtime.openapi.yaml` 提供页面所渲染的生成文档。这些路由不需要凭据,页面也不会发送 API 请求。反向代理不会把 `/docs` 发送到 Core,因此请通过 Core 自己的地址打开:在 Core 主机上访问 `http://127.0.0.1:<port>/docs`,其中端口为 [`ports.core`](../configuration.md#settings),默认是 8091。浏览器从 `unpkg.com` 加载 Swagger UI。

## 机器连接 API {#machine-connection-api}

节点、Runtime 守护进程和自托管安装程序使用各自的凭据调用 `/api/v1`。[机器连接 API](../../../contracts/agents-api/zh/machine-api.md) 列出了每个路由、调用方和凭据。
1 change: 1 addition & 0 deletions scripts/build-core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ trap 'rm -rf "$build_context"' EXIT
tar -C "$repo_root" -cf - \
go.mod go.sum \
contracts/agents-api/v1 \
contracts/agents-api/openapi.go contracts/agents-api/openapi.yaml contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml \
internal/agentdaemon/proto \
internal/runtimefs internal/runtimebootstrap internal/agentnetwork internal/agentbundle internal/agentcapabilities internal/agentplugin internal/agentskill internal/harnessconfig internal/modelprovider internal/providerassets internal/obs/log services/core \
| tar -C "$build_context" -xf -
Expand Down
2 changes: 2 additions & 0 deletions services/core/internal/api/contract_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
// "METHOD /path"; the method * matches every method.
var unpublishedRoutes = map[string]string{
"GET /healthz": "liveness probe, not part of the Agent API",
"GET /docs": "reference page rendering the published contracts",
"GET /docs/{document}": "the published contract documents themselves",
"* /api/v1/agent-daemon/install/*": "public immutable native release content, not an API operation",
}

Expand Down
1 change: 1 addition & 0 deletions services/core/internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (h *Handler) routes() *chi.Mux {
router.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
registerOpenAPIDocsRoutes(router)
router.Group(func(r chi.Router) {
r.Use(h.authenticateProject)
h.registerSkillRoutes(r)
Expand Down
61 changes: 61 additions & 0 deletions services/core/internal/api/openapi_docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package api

import (
"net/http"

agentsapi "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api"
"github.com/go-chi/chi/v5"
)

// openAPIDocsPage renders the embedded documents with a pinned Swagger UI.
// The page stays read-only: no operation can be submitted, the authorization
// controls are removed so it never collects a credential, and validatorUrl
// is null so the browser contacts no validator service.
const openAPIDocsPage = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenAgentCore API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" integrity="sha384-rcbEi6xgdPk0iWkAQzT2F3FeBJXdG+ydrawGlfHAFIZG7wU6aKbQaRewysYpmrlW" crossorigin="anonymous">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui-bundle.js" integrity="sha384-NXtFPpN61oWCuN4D42K6Zd5Rt2+uxeIT36R7kpXBuY9tLnZorzrJ4ykpqwJfgjpZ" crossorigin="anonymous"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui-standalone-preset.js" integrity="sha384-qr68CD0cvHa88PmVu7e1a58Ego4qvKtcvcLdS2a8Mo5zILI01gyIV9jVwJk7X2NU" crossorigin="anonymous"></script>
<script>
SwaggerUIBundle({
dom_id: "#swagger-ui",
urls: [
{url: "/docs/openapi.yaml", name: "Agents API /v1"},
{url: "/docs/core.openapi.yaml", name: "Core API /core/v1"},
{url: "/docs/runtime.openapi.yaml", name: "Machine API /api/v1"}
],
supportedSubmitMethods: [],
validatorUrl: null,
layout: "StandaloneLayout",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [() => ({components: {authorizeBtn: () => null, authorizeOperationBtn: () => null}})]
});
</script>
</body>
</html>
`

// registerOpenAPIDocsRoutes serves the API reference page and the documents
// it renders without authentication; they are the published contracts.
func registerOpenAPIDocsRoutes(router chi.Router) {
router.Get("/docs", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(openAPIDocsPage))
})
router.Get("/docs/{document}", func(w http.ResponseWriter, r *http.Request) {
document, err := agentsapi.OpenAPI.ReadFile(chi.URLParam(r, "document"))
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/yaml")
_, _ = w.Write(document)
})
}
41 changes: 41 additions & 0 deletions services/core/internal/api/openapi_docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api

import (
"net/http"
"os"
"strings"
"testing"
)

// /docs renders every published contract without a credential, and serves
// nothing but those documents.
func TestOpenAPIDocsServeThePublishedContracts(t *testing.T) {
handler, _, _ := routingFixture(t)
page := serve(handler, http.MethodGet, "/docs", "", nil)
if page.Code != http.StatusOK || !strings.HasPrefix(page.Header().Get("Content-Type"), "text/html") {
t.Fatalf("GET /docs = %d %v", page.Code, page.Header())
}
for _, readOnly := range []string{"supportedSubmitMethods: []", "validatorUrl: null", "authorizeBtn: () => null", "authorizeOperationBtn: () => null"} {
if !strings.Contains(page.Body.String(), readOnly) {
t.Errorf("the page lost read-only setting %q", readOnly)
}
}
for _, file := range []string{"openapi.yaml", "core.openapi.yaml", "runtime.openapi.yaml"} {
if !strings.Contains(page.Body.String(), `"/docs/`+file+`"`) {
t.Errorf("the page does not render %s", file)
}
want, err := os.ReadFile("../../../../contracts/agents-api/" + file)
if err != nil {
t.Fatal(err)
}
got := serve(handler, http.MethodGet, "/docs/"+file, "", nil)
if got.Code != http.StatusOK || got.Header().Get("Content-Type") != "application/yaml" || got.Body.String() != string(want) {
t.Errorf("GET /docs/%s = %d %v", file, got.Code, got.Header())
}
}
for _, path := range []string{"/docs/openapi.go", "/docs/upstream.json", "/docs/missing.yaml"} {
if got := serve(handler, http.MethodGet, path, "", nil); got.Code != http.StatusNotFound {
t.Errorf("GET %s = %d, want 404", path, got.Code)
}
}
}
2 changes: 1 addition & 1 deletion services/core/internal/api/routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func dirtyVariants(clean string) []string {
// credential, so it can never reach another route group or skip its checks.
func TestEveryRouteAuthenticatesItsCanonicalPath(t *testing.T) {
handler, router, s := routingFixture(t)
selfAuthenticated := map[string]bool{"GET /healthz": false, "POST /api/v1/sandbox-node/enroll": false, "GET /api/v1/sandbox-node/identity": false, "GET /api/v1/sandbox-node/configuration": false}
selfAuthenticated := map[string]bool{"GET /healthz": false, "GET /docs": false, "GET /docs/{document}": false, "POST /api/v1/sandbox-node/enroll": false, "GET /api/v1/sandbox-node/identity": false, "GET /api/v1/sandbox-node/configuration": false}
credentials := []http.Header{{}, withHeaders(beta), withHeaders([]string{"Authorization", "Bearer " + routingAdminKey}, beta),
withHeaders([]string{"Authorization", "Basic " + routingKey}, beta), withHeaders([]string{"Authorization", "Bearer wrong"}),
withHeaders(project), withHeaders(project, []string{"OpenAI-Beta", "agents=v0"}),
Expand Down
Loading