Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 desktop/src-tauri/src/transport/operations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"contractSha256": "b5631d32c669bbf4b6d340e063ffbbb93a974160380d634ed893dd24b42bb549",
"contractSha256": "e3f06ca7f5e7b0f7b260e3073f29186bd5a0e52ea07c6239828b44d668a1343e",
"operations": {
"get_liveness": {
"method": "GET",
Expand Down
70 changes: 70 additions & 0 deletions desktop/ui/src/generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/server-info": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 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.
*/
get: operations["get_server_info"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/capabilities": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1984,6 +2004,32 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** @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. */
ContractVersion: {
major: number;
minor: number;
};
/** @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. */
FeatureContract: {
version: components["schemas"]["ContractVersion"];
operations: string[];
};
/** @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. */
ServerInfo: {
/** @description Compatibility version for this response shape and field semantics. */
schema_version: components["schemas"]["ContractVersion"];
/** @enum {string} */
product: "powercontext";
/** @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. */
server_id: string;
package_version: string;
/** @description Major/minor projection of the OpenAPI info.version served by this package. */
api_contract_version: components["schemas"]["ContractVersion"];
/** @description Stable feature groups keyed by contract name. */
feature_contracts: {
[key: string]: components["schemas"]["FeatureContract"];
};
};
CreateSubjectSourceRequest: {
subject_key: string;
/**
Expand Down Expand Up @@ -4843,6 +4889,30 @@ export interface operations {
};
};
};
get_server_info: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Stable deployment identity and protocol compatibility metadata. */
200: {
headers: {
"X-PowerContext-Request-ID": components["headers"]["RequestId"];
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ServerInfo"];
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
503: components["responses"]["Unavailable"];
};
};
get_capabilities: {
parameters: {
query?: never;
Expand Down
2 changes: 1 addition & 1 deletion desktop/ui/src/generated/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

// Generated from openapi/powercontext.yaml. Do not edit.
export const contractSha256 = "b5631d32c669bbf4b6d340e063ffbbb93a974160380d634ed893dd24b42bb549";
export const contractSha256 = "e3f06ca7f5e7b0f7b260e3073f29186bd5a0e52ea07c6239828b44d668a1343e";
export const operations = {
"get_liveness": {
"method": "GET",
Expand Down
66 changes: 66 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,69 @@ 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 integers with `major >= 1` and `minor >= 0`. 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.

OpenAPI's root `x-powercontext-feature-contracts` declares the explicit feature versions. Each governed operation lists
its membership in the same extension; `make api-generate` produces the discovery metadata from those declarations.
Generation rejects invalid versions (including values outside these bounds), unknown or duplicate memberships, and
features without operations. Version bumps remain an explicit contract edit, not an automatic consequence of changing membership.

The Server stores one identity singleton using the Runtime-owned 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.
The identity repository retries only SQLite busy/locked errors, replaying the complete schema or singleton operation
after rollback. It admits retries for up to five seconds with 50 ms waits; each SQL attempt also retains the driver's
configured busy timeout. Exhaustion and all other errors propagate; offline rotation is not retried.
An in-memory or temporary SQLite deployment receives a new ID with each database lifetime because it has no durable store. Applications using the same shared-memory SQLite database share both data and identity while any
Runtime connection keeps that database alive; after the last connection closes, reopening creates new data and identity.
SQLite storage classification uses the dialect's effective connection arguments and the decoded SQLite URI, including
supported true spellings (`true`, `1`, `yes`, `on`), percent-encoded `:memory:` paths, and the built-in `vfs=memdb`
in-memory filesystem. Empty-path file URIs such as
`file:?uri=true` and `file:?cache=shared&uri=true` create connection-local temporary databases, not durable files.
Classification also honors SQLite's decoded NUL termination for filenames and query parameter names/values:
`file:%00tail?uri=true` is temporary, an encoded `:memory:` followed by `%00` remains memory storage, and
`mode=memory%2500tail` or `mode%2500tail=memory` in a SQLAlchemy URL still selects native memory mode. Only an exact lowercase `file:` prefix enables
SQLite URI interpretation; uppercase schemes, leading spaces, and literal filename controls are not normalized.
`SQLiteConfig.is_in_memory` distinguishes memory storage; `SQLiteConfig.is_persistent` excludes both memory and temporary
storage. Pooling keeps one connection for nonpersistent storage; offline maintenance, cursor-secret persistence, and
subprocess workers consume that same persistence classification.

The unified migration implementation from [RFC #1771](../rfcs/1771-unified-database-migrations.md) currently
covers only a registered four-table Artifact bundle. It does not manage `pc_server_identity`, gate ordinary Server
startup, or establish complete Server readiness; its maintenance commands reject a complete business database with
unmanaged tables. Do not use that partial bundle to migrate a Server database.

When unified migration takes ownership of the complete Server schema, identity table creation must move from startup
DDL into an immutable managed revision, with schema verification before Runtime composition. If discovery is already
released, the supported historical baseline must include the existing identity table and preserve its singleton value;
if still unmerged at framework enablement, discovery must ship that revision alongside the model. Deployment identity
remains distinct from `pc_schema_revision`, and schema adoption or upgrades must not rotate `server_id`. Clone rotation
remains an explicit offline operation. No independent schema-version or migration-readiness marker is added for identity.

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 and temporary 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 +181,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 +191,7 @@ async def search() -> None:
mode="auto",
)
)
print(server_info.model_dump())
print(capabilities.model_dump())
print(result.model_dump())
```
Expand Down
56 changes: 56 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,59 @@ 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 >= 1` 和 `minor >= 0` 的整数。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 新增的未知可选字段。

OpenAPI 根级 `x-powercontext-feature-contracts` 显式声明 feature version,各受管 operation 使用同名扩展声明归属。
`make api-generate` 从这些声明生成 discovery metadata,并拒绝超出上述范围的版本、未知或重复归属及没有 operation 的 feature。
版本号仍须显式修改,不会因归属变化而自动增加。

Server 使用 Runtime 持有的主关系数据库保存一条 identity singleton。启动时先幂等创建 identity table,再原子创建或读取
singleton,因此并发 initializer 会收敛到同一 ID,进程重启、package 升级、备份恢复以及共享同一数据库的 replica
也会保持该 ID。identity schema 初始化或读取失败时,Server 会在进入 readiness 之前直接启动失败,而不会发布
临时 identity。identity repository 只重试 SQLite busy/locked 错误,在事务回滚后重新执行完整的 schema 或 singleton
操作。重试窗口为五秒,每次等待 50 ms;每条 SQL 仍使用 driver 配置的 busy timeout。窗口耗尽或其他错误会直接
传播,离线轮换不重试。内存或临时 SQLite 没有持久存储,所以每个数据库生命周期都会获得新 ID。
使用同一共享内存 SQLite 数据库的 application 会共享数据和 identity;只要仍有 Runtime 连接,数据库就保持存活。
最后一个连接关闭后,再次打开会重新创建数据和 identity。
SQLite 存储分类使用 dialect 的实际连接参数和解码后的 URI,包括支持的 true 拼写(`true`、`1`、`yes`、`on`)以及
百分号编码的 `:memory:` path 以及内置的 `vfs=memdb` 内存文件系统。`file:?uri=true` 和 `file:?cache=shared&uri=true` 这类空路径 URI 创建的是连接独占的
临时数据库,而非持久文件。分类也遵循 SQLite 在解码后的 NUL 处终止文件名和查询参数名/值的行为:`file:%00tail?uri=true` 属于临时存储,
编码后的 `:memory:` 即使后接 `%00` 也仍是内存存储,SQLAlchemy URL 中的 `mode=memory%2500tail` 或
`mode%2500tail=memory` 也仍会选择原生 memory mode。只有精确的小写 `file:` 前缀才启用 SQLite URI 解释,
不会归一化大写 scheme、前导空格或文件名中的控制字符。`SQLiteConfig.is_in_memory` 区分内存存储;`SQLiteConfig.is_persistent` 同时排除内存与临时
存储。非持久存储的连接池保留一个连接;离线维护、cursor secret 持久化及子进程 worker 统一使用该持久性分类。

[RFC #1771](../rfcs/1771-unified-database-migrations.md) 对应的统一迁移实现目前只覆盖已注册的四张 Artifact 表。
它尚未管理 `pc_server_identity`、接管普通 Server 启动检查或证明完整 Server readiness;维护命令会拒绝包含未管理表的
完整业务数据库。不能使用这个局部 bundle 迁移 Server 数据库。

统一迁移接管完整 Server schema 时,identity table 创建必须从启动 DDL 移到不可变的受管 revision,并在 Runtime
组装前验证 schema。若 discovery 已发布,支持的历史 baseline 必须包含现有 identity table,并保留 singleton 值;
若在框架启用时仍未合并,discovery 必须随 model 一起提交该 revision。deployment identity 与 `pc_schema_revision`
保持独立,schema 纳管或升级不能轮换 `server_id`。clone 轮换仍是显式离线操作,identity 不另设 schema version 或
migration readiness marker。

把备份恢复为原 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 +167,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 +177,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 @@ -225,6 +225,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
Loading
Loading