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 .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
uses: ./.github/actions/setup-python-env

- name: Build documentation
run: uv run zensical build --clean
run: make docs-build

- name: Upload artifact
uses: actions/upload-pages-artifact@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ jobs:
uses: ./.github/actions/setup-python-env

- name: Build documentation
run: uv run zensical build --clean
run: make docs-build

- name: Upload artifact
uses: actions/upload-pages-artifact@v5
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ venv.bak/

# mkdocs/zensical documentation
/site
/docs/api/openapi.yaml
e2e/bub/results/

# mypy
Expand Down
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,20 @@ publish: ## Publish a release to PyPI.
.PHONY: build-and-publish
build-and-publish: build publish ## Build and publish.

.PHONY: docs-build
docs-build: ## Build the documentation and publish the canonical OpenAPI contract.
@install -D -m 0644 openapi/powercontext.yaml docs/api/openapi.yaml
@trap 'rm -f docs/api/openapi.yaml' EXIT; uv run zensical build --clean -s

.PHONY: docs-test
docs-test: ## Test if documentation can be built without warnings or errors
@uv run zensical build -s
docs-test: docs-build ## Test if documentation can be built without warnings or errors
@test -f site/api/index.html
@cmp --silent openapi/powercontext.yaml site/api/openapi.yaml

.PHONY: docs
docs: ## Build and serve the documentation
@uv run zensical serve
@install -D -m 0644 openapi/powercontext.yaml docs/api/openapi.yaml
@trap 'rm -f docs/api/openapi.yaml' EXIT; uv run zensical serve $(ARGS)

.PHONY: help
help:
Expand Down
39 changes: 39 additions & 0 deletions docs/api/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!--
~ Copyright (c) 2026 OceanBase.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!doctype html>
<html lang="en">
<head>
<title>PowerContext HTTP API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.66.1"></script>
<script>
Scalar.createApiReference("#app", {
url: "./openapi.yaml",
agent: { disabled: true },
hideClientButton: true,
hideTestRequestButton: true,
showDeveloperTools: "never",
telemetry: false,
withDefaultFonts: false,
});
</script>
<noscript>JavaScript is required to render the PowerContext HTTP API reference.</noscript>
</body>
</html>
10 changes: 6 additions & 4 deletions docs/en/docs/reference/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,12 @@ want the supplied SQLite or OceanBase-backed implementation in the same process.
## HTTP and MCP

See [HTTP API](http-api.md) for authentication, curl examples, operation groups, errors, and the complete OpenAPI
contract. The Server publishes its OpenAPI document at `/openapi.json`, readiness at `/health/ready`, capabilities at
`/v1/capabilities`, and Streamable HTTP MCP at `/mcp` by default. HTTP is the complete application contract. MCP is a
curated agent-facing projection of Memory and Candidate Review operations. The five Candidate Review operations use
the same validation, `expected_version` concurrency checks, and approval transaction over HTTP and MCP.
contract. The Server publishes a Scalar API reference at `/docs`, its OpenAPI document at `/openapi.json`, readiness at
`/health/ready`, capabilities at `/v1/capabilities`, and Streamable HTTP MCP at `/mcp` by default. The Scalar reference
remains public when bearer authentication is enabled, but the operations it describes retain their normal authentication
requirements. HTTP is the complete application contract. MCP is a curated agent-facing projection of Memory and
Candidate Review operations. The five Candidate Review operations use the same validation, `expected_version`
concurrency checks, and approval transaction over HTTP and MCP.
Readiness is `ready` with HTTP 200 when all checks pass, `degraded` with HTTP 200 when only configured inference checks
fail, and `not_ready` with HTTP 503 when the Runtime or database fails. Dependency checks use `ready`, `unavailable`,
`timeout`, or `misconfigured`; an intentionally unbound Runtime reports `not_ready` for the `runtime` check.
Expand Down
13 changes: 7 additions & 6 deletions docs/zh/docs/reference/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,13 @@ Discovery 不进入 Review。显式调用 `import_external_skill` 并提供精

## HTTP 和 MCP

鉴权、curl 示例、操作分组、错误格式和完整 OpenAPI 契约见 [HTTP API](http-api.md)。Server 在
`/openapi.json` 提供 OpenAPI 文档,在 `/health/ready` 提供就绪检查,在 `/v1/capabilities`
提供能力信息,并默认在 `/mcp` 提供 Streamable HTTP MCP。HTTP 是完整应用契约,MCP 是面向 Agent 的
Memory 与 Candidate Review operation 子集。五个 Candidate Review operation 通过 HTTP 和 MCP 使用相同的
validation、`expected_version` 并发校验和 approval transaction。Experience/Skill generation、exact read、
external Registry operation 和低阶 proposal operation 仍只通过 HTTP 提供。
鉴权、curl 示例、操作分组、错误格式和完整 OpenAPI 契约见 [HTTP API](http-api.md)。Server 在 `/docs` 提供
Scalar API reference,在 `/openapi.json` 提供 OpenAPI 文档,在 `/health/ready` 提供就绪检查,在
`/v1/capabilities` 提供能力信息,并默认在 `/mcp` 提供 Streamable HTTP MCP。启用 Bearer authentication 后,
Scalar reference 仍可公开访问,但其中描述的 operation 继续遵守各自的认证要求。HTTP 是完整应用契约,MCP 是
面向 Agent 的 Memory 与 Candidate Review operation 子集。五个 Candidate Review operation 通过 HTTP 和 MCP
使用相同的 validation、`expected_version` 并发校验和 approval transaction。Experience/Skill generation、
exact read、external Registry operation 和低阶 proposal operation 仍只通过 HTTP 提供。
所有检查通过时 readiness 为 HTTP 200 的 `ready`;只有已配置的推理检查失败时为 HTTP 200 的 `degraded`;
Runtime 或数据库失败时为 HTTP 503 的 `not_ready`。依赖检查使用 `ready`、`unavailable`、`timeout` 或
`misconfigured`;有意不绑定 Runtime 时,`runtime` 检查使用 `not_ready`。
Expand Down
3 changes: 2 additions & 1 deletion e2e/bub/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion integrations/bub/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ requires-python = ">=3.12,<4.0"
dependencies = [
"bub>=0.4.0,<0.5.0",
"httpx>=0.28,<1",
"powercontext[client]>=0.0.3",
"powercontext[client]",
"pydantic-settings>=2.7,<3",
]

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ server = [
"powercontext[builtin]",
"prometheus-client>=0.21,<1",
"pydantic-settings>=2.7,<3",
"scalar-fastapi>=1.8.2,<2",
"uvicorn>=0.34,<1",
]
tracing-otlp = [
Expand Down
25 changes: 25 additions & 0 deletions src/powercontext/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from opentelemetry.trace import SpanKind
from scalar_fastapi import AgentScalarConfig, get_scalar_api_reference
from starlette.middleware import Middleware
from starlette.middleware.base import RequestResponseEndpoint
from starlette.types import Lifespan
Expand Down Expand Up @@ -390,6 +391,7 @@
from powercontext.server.metrics import ServerMetrics
from powercontext.server.tracing import ServerTracing

_SCALAR_JS_URL = "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.66.1"
REQUEST_ID_HEADER = "X-PowerContext-Request-ID"
REPORT_SELECTION_DIGEST_HEADER = "X-PowerContext-Selection-Digest"
REPORT_DIGEST_HEADER = "X-PowerContext-Report-Digest"
Expand Down Expand Up @@ -571,6 +573,8 @@ def create_app(
title=API_TITLE,
version=API_VERSION,
description=API_DESCRIPTION,
docs_url=None,
redoc_url=None,
lifespan=lifespan,
middleware=list(middleware),
)
Expand Down Expand Up @@ -686,6 +690,12 @@ async def unexpected_error(request: Request, error: Exception) -> JSONResponse:
_add_route(app, APPROVE_ARTIFACT_CANDIDATE, approve_artifact_candidate)
_add_route(app, REJECT_ARTIFACT_CANDIDATE, reject_artifact_candidate)
_add_route(app, REVISE_ARTIFACT_CANDIDATE, revise_artifact_candidate)
app.add_api_route(
"/docs",
scalar_api_reference,
include_in_schema=False,
methods=["GET"],
)

def canonical_openapi() -> dict[str, Any]:
if app.openapi_schema is None:
Expand All @@ -701,6 +711,21 @@ def canonical_openapi() -> dict[str, Any]:
return app


async def scalar_api_reference(request: Request) -> Response:
"""Render the runtime OpenAPI contract with Scalar."""

return get_scalar_api_reference(
content=request.app.openapi(),
title=f"{API_TITLE} Reference",
scalar_js_url=_SCALAR_JS_URL,
scalar_favicon_url="data:,",
with_default_fonts=False,
show_developer_tools="never",
telemetry=False,
agent=AgentScalarConfig(disabled=True),
)


async def get_liveness() -> HealthResponse:
return HealthResponse(status="ok")

Expand Down
2 changes: 1 addition & 1 deletion src/powercontext/server/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from powercontext.http import ErrorDetail, ErrorResponse
from powercontext.server.context import is_internal_bridge

_PUBLIC_PATHS = frozenset({"/", "/handoff-reports", "/reviews", "/skills", "/health/live", "/health/ready"})
_PUBLIC_PATHS = frozenset({"/", "/docs", "/handoff-reports", "/reviews", "/skills", "/health/live", "/health/ready"})
_PUBLIC_PATH_PREFIXES = ("/static/",)


Expand Down
16 changes: 16 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,20 @@ def test_liveness_adds_a_server_owned_request_id() -> None:
assert "X-Request-ID" not in response.headers


def test_scalar_reference_embeds_the_canonical_openapi_contract() -> None:
client = TestClient(create_app())
response = client.get("/docs")

assert response.status_code == 200
assert response.headers["content-type"].startswith("text/html")
assert "PowerContext API Reference" in response.text
assert '"openapi": "3.0.3"' in response.text
assert '"/v1/context/prepare"' in response.text
assert "@scalar/api-reference@1.66.1" in response.text
assert "proxyUrl" not in response.text
assert client.get("/scalar").status_code == 404


def test_server_factory_optionally_requires_bearer_authentication() -> None:
app = create_server_app(
settings=ServerSettings(
Expand All @@ -291,6 +305,7 @@ def test_server_factory_optionally_requires_bearer_authentication() -> None:
protected_metrics = client.get("/metrics")
accepted_metrics = client.get("/metrics", headers={"Authorization": "Bearer server-secret"})
liveness = client.get("/health/live")
scalar_reference = client.get("/docs")

assert missing.status_code == 401
assert missing.headers["WWW-Authenticate"] == "Bearer"
Expand All @@ -307,6 +322,7 @@ def test_server_factory_optionally_requires_bearer_authentication() -> None:
assert protected_metrics.status_code == 401
assert accepted_metrics.status_code == 200
assert liveness.status_code == 200
assert scalar_reference.status_code == 200


def test_readiness_reports_unavailable_bindings() -> None:
Expand Down
11 changes: 11 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ nav = [
{ "Interfaces" = "en/docs/reference/interfaces.md" },
{ "HTTP API" = "en/docs/reference/http-api.md" },
{ "Configuration" = "en/docs/reference/configuration.md" },
{ "HTTP API Reference" = "api/index.html" },
] },
] },
{ "Development" = [
Expand Down Expand Up @@ -127,6 +128,7 @@ nav = [
{ "接口" = "zh/docs/reference/interfaces.md" },
{ "HTTP API" = "zh/docs/reference/http-api.md" },
{ "配置" = "zh/docs/reference/configuration.md" },
{ "HTTP API 参考" = "api/index.html" },
] },
] },
{ "开发" = [
Expand Down
Loading