Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
35 changes: 35 additions & 0 deletions docs/en/development/remote-access-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The source contract is `openapi/powercontext.yaml`. Generated Pydantic models an
| Area | Operations |
| --- | --- |
| Health | liveness and readiness |
| Server discovery | stable deployment identity and protocol contracts |
| Capabilities | source types, Artifact families, extraction, search modes |
| Sources | capture durable content evidence |
| Memory | flush pending Sources, remember explicit entries, search |
Expand All @@ -100,6 +101,38 @@ Server errors use the OpenAPI error schema and include a Server-owned `X-PowerCo
derived from the inbound request span. Validation errors, revision conflicts, missing entries, unavailable inference,
and internal failures map to stable HTTP status codes.

### Server identity and compatibility discovery

`GET /v1/server-info` is protected by `server.observe` and returns the stable deployment `server_id`, installed package
version, API contract version, response schema version, and initial feature contracts. It deliberately does not report
health, enabled runtime capabilities, limits, inventory, secrets, filesystem paths, or the authenticated principal.
Use the dedicated health, capabilities, statistics, and access endpoints for those concerns.

All discovery versions use `major` and `minor` integers. A major increment may remove or incompatibly change the
governed contract; a minor increment is backward compatible. The response `schema_version` governs its fields and
semantics, while `api_contract_version` is the major/minor projection of the OpenAPI `info.version`. Each feature
contract version applies only to its listed OpenAPI operation IDs: adding an operation or compatible semantics increments
minor; removing, renaming, or incompatibly changing a listed operation increments major. Compatible clients must ignore
unknown optional fields introduced by a schema minor version.

The Server stores one identity singleton in the configured primary relational database. Startup creates the identity
table idempotently, then atomically creates or loads the singleton, so concurrent initializers converge on one ID and
restarts, package upgrades, backup restore, and replicas sharing that database retain it. If identity schema
initialization or loading fails, Server startup fails before readiness instead of publishing a temporary identity. An
in-memory SQLite deployment, including SQLite URI memory mode, receives a new ID with each database lifetime because it
has no durable store.

Treat a restored backup as the same deployment and keep its ID. When a backup is used to create an independent clone,
stop every Server process using the clone database and rotate only the clone:

```bash
uv run powercontext server identity-reset --env-file /path/to/clone.env --maintenance-confirmed
```

The command refuses in-memory databases and requires the explicit maintenance confirmation. It cannot detect active
replicas, so stopping them is an operator precondition. Logical application-data imports do not copy the identity unless
the `pc_server_identity` table itself is included.

## Python Client

Install the Client role for the SDK:
Expand All @@ -117,6 +150,7 @@ from powercontext.client import PowerContextClient

async def search() -> None:
async with PowerContextClient("http://127.0.0.1:8000") as client:
server_info = await client.get_server_info()
capabilities = await client.get_capabilities()
result = await client.search_memory(
SearchMemoryRequest(
Expand All @@ -126,6 +160,7 @@ async def search() -> None:
mode="auto",
)
)
print(server_info.model_dump())
print(capabilities.model_dump())
print(result.model_dump())
```
Expand Down
31 changes: 31 additions & 0 deletions docs/zh/development/remote-access-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ inference 配置见[配置 Pydantic AI 推理](pydantic-ai-inference.md)。
| 领域 | Operation |
| --- | --- |
| Health | liveness 和 readiness |
| Server discovery | 稳定 deployment identity 和协议契约 |
| Capabilities | source type、Artifact family、extraction、search mode |
| Sources | capture 持久化 content evidence |
| Memory | flush 待处理 Source、remember 显式 entry、search |
Expand All @@ -96,6 +97,34 @@ Server error 使用 OpenAPI error schema,并在 response header 中包含由 i
Server-owned `X-PowerContext-Request-ID`。validation error、revision conflict、entry 不存在、inference
unavailable 和内部 failure 会映射为稳定的 HTTP status code。

### Server identity 与兼容性发现

`GET /v1/server-info` 受 `server.observe` 保护,返回稳定的 deployment `server_id`、已安装 package version、API
contract version、response schema version 和首批 feature contract。它刻意不返回 health、已启用 runtime
capability、limit、inventory、secret、文件系统 path 或已认证 principal;这些信息分别由 health、capabilities、
statistics 和 access endpoint 负责。

所有 discovery version 都使用 `major` 和 `minor` 整数。major 增加表示受管契约可能被移除或发生不兼容变化;minor
增加只允许向后兼容的扩展。response 的 `schema_version` 管理字段与语义,`api_contract_version` 是 OpenAPI
`info.version` 的 major/minor 投影。每个 feature contract version 只约束它列出的 OpenAPI operation ID:增加
operation 或兼容语义时增加 minor,移除、重命名或不兼容地改变已列 operation 时增加 major。兼容 client 必须忽略
schema minor version 新增的未知可选字段。

Server 在配置的主关系数据库中保存一条 identity singleton。启动时先幂等创建 identity table,再原子创建或读取
singleton,因此并发 initializer 会收敛到同一 ID,进程重启、package 升级、备份恢复以及共享同一数据库的 replica
也会保持该 ID。identity schema 初始化或读取失败时,Server 会在进入 readiness 之前直接启动失败,而不会发布
临时 identity。内存 SQLite(包括 SQLite URI memory mode)没有持久存储,所以每个数据库生命周期都会获得新 ID。

把备份恢复为原 deployment 时应保留原 ID。若用备份创建独立 clone,请停止所有使用 clone 数据库的 Server
进程,然后只在 clone 上轮换:

```bash
uv run powercontext server identity-reset --env-file /path/to/clone.env --maintenance-confirmed
```

该命令拒绝内存数据库,并要求显式 maintenance confirmation。它无法检测仍在运行的 replica,因此停服是 operator
前置条件。逻辑 application-data import 不会复制 identity,除非显式包含 `pc_server_identity` 表。

## Python Client

安装 Client role 以使用 SDK:
Expand All @@ -113,6 +142,7 @@ from powercontext.client import PowerContextClient

async def search() -> None:
async with PowerContextClient("http://127.0.0.1:8000") as client:
server_info = await client.get_server_info()
capabilities = await client.get_capabilities()
result = await client.search_memory(
SearchMemoryRequest(
Expand All @@ -122,6 +152,7 @@ async def search() -> None:
mode="auto",
)
)
print(server_info.model_dump())
print(capabilities.model_dump())
print(result.model_dump())
```
Expand Down
11 changes: 11 additions & 0 deletions integrations/dsh/plugins/powercontext/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ const OPERATIONS$1 = {
successStatuses: [200],
emptyStatuses: []
},
get_server_info: {
method: "GET",
path: "/v1/server-info",
location: null,
scopeMode: "none",
pathParameters: [],
queryParams: [],
headerParams: [],
successStatuses: [200],
emptyStatuses: []
},
get_capabilities: {
method: "GET",
path: "/v1/capabilities",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const OPERATIONS = {
flush_profile: { method: 'POST', path: '/v1/profile/flush', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_liveness: { method: 'GET', path: '/health/live', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_readiness: { method: 'GET', path: '/health/ready', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_server_info: { method: 'GET', path: '/v1/server-info', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_capabilities: { method: 'GET', path: '/v1/capabilities', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
list_scopes: { method: 'GET', path: '/v1/scopes', location: "query", scopeMode: 'none', pathParameters: [], queryParams: ['query','query_field','parent_scope_id','external_reference_kind','binding_integration','binding_kind','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_scope: { method: 'POST', path: '/v1/scopes', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const OPERATIONS = {
flush_profile: { method: 'POST', path: '/v1/profile/flush', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_liveness: { method: 'GET', path: '/health/live', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_readiness: { method: 'GET', path: '/health/ready', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_server_info: { method: 'GET', path: '/v1/server-info', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_capabilities: { method: 'GET', path: '/v1/capabilities', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
list_scopes: { method: 'GET', path: '/v1/scopes', location: "query", scopeMode: 'none', pathParameters: [], queryParams: ['query','query_field','parent_scope_id','external_reference_kind','binding_integration','binding_kind','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_scope: { method: 'POST', path: '/v1/scopes', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const OPERATIONS = {
flush_profile: { method: 'POST', path: '/v1/profile/flush', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_liveness: { method: 'GET', path: '/health/live', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_readiness: { method: 'GET', path: '/health/ready', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_server_info: { method: 'GET', path: '/v1/server-info', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_capabilities: { method: 'GET', path: '/v1/capabilities', location: null, scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
list_scopes: { method: 'GET', path: '/v1/scopes', location: "query", scopeMode: 'none', pathParameters: [], queryParams: ['query','query_field','parent_scope_id','external_reference_kind','binding_integration','binding_kind','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_scope: { method: 'POST', path: '/v1/scopes', location: "body", scopeMode: 'none', pathParameters: [], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
Expand Down
90 changes: 89 additions & 1 deletion openapi/powercontext.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ openapi: 3.0.3
info:
title: PowerContext API
description: Remote PowerContext transport. Runtime behavior is reported by /v1/capabilities.
version: 1.1.0
version: 1.2.0
security:
- BearerAuth: []
- {}
Expand Down Expand Up @@ -242,6 +242,31 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ReadinessResponse"
/v1/server-info:
get:
tags: [server]
summary: Get stable Server deployment identity and protocol contracts
description: >-
Returns deployment identity and compatibility metadata only. Runtime availability, enabled behavior,
limits and inventory remain owned by health, capabilities and statistics endpoints.
operationId: get_server_info
x-powercontext-access: {action: server.observe, resource: {type: server}}
responses:
"200":
description: Stable deployment identity and protocol compatibility metadata.
headers:
X-PowerContext-Request-ID:
$ref: "#/components/headers/RequestId"
content:
application/json:
schema:
$ref: "#/components/schemas/ServerInfo"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"503":
$ref: "#/components/responses/Unavailable"
/v1/capabilities:
get:
tags: [capabilities]
Expand Down Expand Up @@ -4450,6 +4475,69 @@ components:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
ContractVersion:
type: object
description: >-
A major/minor compatibility version. A major increment may remove or incompatibly change the governed
contract. A minor increment only adds backward-compatible behavior or fields.
required: [major, minor]
properties:
major:
type: integer
minimum: 1
minor:
type: integer
minimum: 0
FeatureContract:
type: object
description: >-
Compatibility version for exactly the listed OpenAPI operation IDs. Adding operations or compatible
semantics increments minor; removing, renaming or incompatibly changing a listed operation increments major.
required: [version, operations]
properties:
version:
$ref: "#/components/schemas/ContractVersion"
operations:
type: array
minItems: 1
uniqueItems: true
items:
type: string
ServerInfo:
type: object
description: >-
Stable deployment identity and protocol compatibility metadata. Compatible clients must ignore unknown
optional fields added by a future schema minor version. This contract intentionally excludes runtime
capabilities, health, limits, inventory and authorization-principal identity.
required:
[schema_version, product, server_id, package_version, api_contract_version, feature_contracts]
properties:
schema_version:
allOf:
- $ref: "#/components/schemas/ContractVersion"
description: Compatibility version for this response shape and field semantics.
product:
type: string
enum: [powercontext]
server_id:
type: string
minLength: 1
maxLength: 128
description: >-
Opaque identity of the durable Server deployment. It is unrelated to Access deployment_id and remains
stable across restarts, upgrades, backup restore and replicas sharing the same primary database.
package_version:
type: string
minLength: 1
api_contract_version:
allOf:
- $ref: "#/components/schemas/ContractVersion"
description: Major/minor projection of the OpenAPI info.version served by this package.
feature_contracts:
type: object
description: Stable feature groups keyed by contract name.
additionalProperties:
$ref: "#/components/schemas/FeatureContract"
CreateSubjectSourceRequest:
type: "object"
additionalProperties: false
Expand Down
8 changes: 6 additions & 2 deletions src/powercontext/builtin/persistence/sqlite/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,12 @@ async def open(


def _is_memory_url(value: str) -> bool:
database = make_url(value).database
return database in {None, "", ":memory:"}
url = make_url(value)
database = url.database
if database in {None, "", ":memory:"}:
return True
uri = str(url.query.get("uri", "")).casefold() == "true"
return uri and (database == "file::memory:" or str(url.query.get("mode", "")).casefold() == "memory")


def _create_database_directory(value: str) -> None:
Expand Down
7 changes: 7 additions & 0 deletions src/powercontext/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
SearchMemoryResponse,
SearchTopicMemoryRequest,
SearchTopicMemoryResponse,
ServerInfo,
SetDefaultScopeRequest,
SetScopeBindingRequest,
SkillArtifact,
Expand Down Expand Up @@ -246,6 +247,7 @@
GET_PROMPT_CONFIGURATION,
GET_READINESS,
GET_SCOPE,
GET_SERVER_INFO,
GET_SKILL,
GET_SKILL_PACKAGE_MANIFEST,
GET_SOURCE,
Expand Down Expand Up @@ -387,6 +389,11 @@ async def get_capabilities(self) -> Capabilities:

return await self._request(GET_CAPABILITIES)

async def get_server_info(self) -> ServerInfo:
"""Read stable deployment identity and protocol compatibility metadata."""

return await self._request(GET_SERVER_INFO)

async def list_scopes(
self,
query: str | None = None,
Expand Down
6 changes: 6 additions & 0 deletions src/powercontext/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
ContextAssemblySection,
ContextReference,
ContinueHandoffRequest,
ContractVersion,
CreateAccessBindingRequest,
CreateArtifactRequest,
CreateDreamRunRequest,
Expand Down Expand Up @@ -149,6 +150,7 @@
FailureSignature,
FailureVerification,
FamilyCount,
FeatureContract,
FinalizeHandoffRequest,
FlushMemoryRequest,
FlushMemoryResponse,
Expand Down Expand Up @@ -349,6 +351,7 @@
SearchTopicMemoryRequest,
SearchTopicMemoryResponse,
ServerAccessResource,
ServerInfo,
SetDefaultScopeRequest,
SetScopeBindingRequest,
SkillArtifact,
Expand Down Expand Up @@ -490,6 +493,7 @@
"ContextAssemblySection",
"ContextReference",
"ContinueHandoffRequest",
"ContractVersion",
"CreateAccessBindingRequest",
"CreateArtifactRequest",
"CreateDreamRunRequest",
Expand Down Expand Up @@ -543,6 +547,7 @@
"FailureSignature",
"FailureVerification",
"FamilyCount",
"FeatureContract",
"FinalizeHandoffRequest",
"FlushMemoryRequest",
"FlushMemoryResponse",
Expand Down Expand Up @@ -743,6 +748,7 @@
"SearchTopicMemoryRequest",
"SearchTopicMemoryResponse",
"ServerAccessResource",
"ServerInfo",
"SetDefaultScopeRequest",
"SetScopeBindingRequest",
"SkillArtifact",
Expand Down
Loading
Loading