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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ can proceed without provider promotion. Section 1.4 defines their boundary;
the [TS execution cards](typescript-control-plane-migration-v0.md#execution-cards-after-the-current-stack)
still own business-rule consolidation and legacy-caller deletion.

### Local provider opening boundary (2026-09-13)

The local runtime now resolves File, SQLite, and the medium-term PostgreSQL
profile through one typed provider handle. No selector means the explicit File
default; a SQLite selector remains an opt-in local profile; a PostgreSQL
selector is accepted only with a service-owned factory that supplies the
provider-neutral `AuthorityStore`. The selector contains no credentials or
database client and binds the selected store identity before any command runs.

This boundary removes per-command provider construction and corrects the
observable source label for injected PostgreSQL stores. A selected-provider
failure preserves its source and fails closed; it cannot fall back to File or
Markdown. The refactor prepares the File/SQLite default path and a switchable
PostgreSQL deployment without changing promotion, D2 soak/retention, or D3
whole-Goal cutover holds.

## Document map and maintenance contract

This RFC separates durable decisions from delivery evidence:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ head 之外。
第 1.4 节明确边界;[TS 执行卡](typescript-control-plane-migration-v0.zh-CN.md)
继续负责业务规则收敛及旧 caller 删除。

### Local provider opening 边界(2026-09-13)

Local runtime 现在通过一个 typed provider handle 解析 File、SQLite 与中期
PostgreSQL profile。没有 selector 时使用明确的 File 默认值;SQLite selector 仍是
opt-in local profile;PostgreSQL selector 只有在 service-owned factory 提供
provider-neutral `AuthorityStore` 时才接受。Selector 不包含凭据或 database client,
并在任何 command 执行前绑定所选 store identity。

这个边界删除了每个 command 各自构造 provider 的重复,并修正了注入 PostgreSQL
store 时对外 source label 的错误标记。已选择 provider 的失败保留 source 并 fail
closed,不能回退到 File 或 Markdown。本次重构为 File/SQLite 默认路径与可切换
PostgreSQL deployment 做准备,不改变 promotion、D2 soak/retention 或 D3 整 Goal
cutover hold。

## 文档地图与维护约定

本文将稳定决策与交付证据分开维护:
Expand Down
17 changes: 17 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ native creation, archival, receipt replay, and store reopen are tested without
Markdown metadata. Python only adapts the typed read result to the compatibility
summary. This is a contract checkpoint, not a completed CLI lifecycle cutover.

### Local provider opening boundary (2026-09-13)

The provider-first runtime now has one typed local opening seam. An absent
selector resolves explicitly to the File profile (`source_authority=file_v0`);
the same handle reports SQLite when the qualified local selector is present and
can report PostgreSQL only through a service-owned factory. Runtime commands no
longer repeat provider construction or infer a PostgreSQL store as File from an
`AuthorityStore` implementation.

The selector carries only provider, goal, tenant, and store-incarnation facts.
It never carries credentials or a database client. Selected-provider failures
retain their provider source and fail closed; they do not silently fall back to
File or Markdown. This is the default provider boundary and TypeScript
ownership refactor, not a SQLite promotion, a whole-Goal cutover, or a
PostgreSQL service claim. Existing promotion, soak, retention, and writer-fence
holds remain unchanged.

Provider-first `todo update --text/--note` preserves claim-neutral correction:
a registered, non-excluded actor may edit an unclaimed active, non-completed
agent Todo, subject to its agent binding. It must not introduce `claimed_by`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ coordination 路径使用同一份语言中立的 `coordination_state_contract_v
仅将 typed read result 适配为兼容 summary。这是 contract 检查点,不是已经完成的
CLI lifecycle cutover。

### Local provider opening 边界(2026-09-13)

Provider-first runtime 现在只有一个 typed local opening seam。没有 selector 时
明确解析为 File profile(`source_authority=file_v0`);存在经过资格验证的 local
selector 时,同一个 handle 报告 SQLite;只有通过 service-owned factory 才能报告
PostgreSQL。runtime command 不再重复构造 provider,也不会因为某个
`AuthorityStore` 实现而把 PostgreSQL 误报成 File。

Selector 只携带 provider、goal、tenant 和 store-incarnation facts,不携带凭据或
database client。已选择 provider 的失败保留其 provider source 并 fail closed,绝不
静默回退到 File 或 Markdown。这是默认 provider 边界与 TypeScript ownership 的重构,
不是 SQLite promotion、整 Goal cutover 或 PostgreSQL service 已交付的声明。现有
promotion、soak、retention 与 writer-fence hold 均保持不变。

Provider-first `todo update --text/--note` 保留不改变认领关系的文案修正:
已注册、未被排除且符合 agent binding 的 actor,可以编辑未认领、active 且未完成的
agent Todo,不得因此写入 `claimed_by`;其他 claim owner 的 Todo 仍拒绝修改。
Expand Down
58 changes: 58 additions & 0 deletions docs/reference/local-authority-provider-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Local authority provider selection

LoopX now has one typed local-provider boundary for every provider-first
coordination command. When a goal has no selector, the boundary resolves the
`file` profile (`source_authority=file_v0`). This makes File/SQLite provider
semantics the default local contract without silently promoting an existing
Markdown goal or changing its writer fence.

## Selection contract

`openLocalAuthorityStoreHandle(runtime_root, goal_id)` resolves a handle with:

| Field | Meaning |
| --- | --- |
| `store` | The provider-neutral `AuthorityStore` implementation |
| `provider` | `file`, `sqlite`, or `postgresql` |
| `sourceAuthority` | The provider evidence label (`*_v0`) |

An absent selector is the explicit default File profile. A SQLite selector uses
the existing `loopx_local_authority_provider_v0` marker and its database
incarnation. A PostgreSQL selector uses the same marker schema plus a
`tenant_id` and `postgresql:<32 lowercase hex>` store identity.

The PostgreSQL marker contains no URL, credential, or database client. Opening
it requires a service-owned `openPostgresqlStore` factory. The factory receives
only the validated public binding facts and must return a PostgreSQL-labelled
`AuthorityStore` whose identity matches the selector. This is the runtime seam
for the medium-term switchable PostgreSQL profile; it does not ship an
authenticated service or grant an Agent database access.

## Failure and compatibility rules

- A selected provider never falls back to File when its selector, database,
factory, identity, or metadata is unavailable.
- `source_authority` identifies the selected provider even when opening it
fails; unresolved or malformed selection reports `null`.
- `decision_read_from_provider` is false for selection/open failures, and
`legacy_fallback_used` remains false.
- The legacy `openLocalAuthorityStore` function still returns only the store,
so existing callers remain source-compatible. Runtime entrypoints use one
shared opening seam and no longer duplicate provider construction.
- Provider identity is observability metadata. It does not decide Todo
eligibility, claims, leases, receipts, or promotion.

The default profile is a routing decision, not a migration. Existing Markdown
state, writer fences, qualification gates, and explicit File/SQLite promotion
holds remain unchanged. SQLite stays an opt-in qualified candidate until the
shared-authority RFC's D2 evidence and owner approval are complete. PostgreSQL
remains an independent service-provider qualification path.

## Validation

The provider selection matrix is exercised with the production-scale synthetic
coordination fixture. Tests cover the default File handle, SQLite persistence,
selected-provider failure without fallback, PostgreSQL factory identity
fencing, and the factory's rejection of a different provider. File, SQLite,
and PostgreSQL continue to share the provider-neutral transaction conformance
contract; PostgreSQL's real-server qualification remains a separate gate.
54 changes: 54 additions & 0 deletions docs/reference/local-authority-provider-selection.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Local Authority Provider 选择

LoopX 现在为所有 provider-first coordination command 提供一个 typed 的
local-provider 边界。Goal 没有 selector 时,边界解析为 `file` profile
(`source_authority=file_v0`)。这使 File/SQLite provider 语义成为默认的
local contract,同时不会静默晋升已有的 Markdown Goal,也不会改变其 writer
fence。

## 选择 contract

`openLocalAuthorityStoreHandle(runtime_root, goal_id)` 返回一个 handle:

| 字段 | 含义 |
| --- | --- |
| `store` | 与 provider 无关的 `AuthorityStore` 实现 |
| `provider` | `file`、`sqlite` 或 `postgresql` |
| `sourceAuthority` | provider 证据标签(`*_v0`) |

没有 selector 时使用显式的默认 File profile。SQLite selector 继续使用已有
的 `loopx_local_authority_provider_v0` marker 及其 database incarnation。
PostgreSQL selector 使用同一 marker schema,并额外绑定 `tenant_id` 与
`postgresql:<32 位小写十六进制>` store identity。

PostgreSQL marker 不包含 URL、凭据或 database client。打开它必须提供由
service 持有的 `openPostgresqlStore` factory。factory 只接收经过校验的公开
binding facts,并且必须返回带 PostgreSQL 标签、且 identity 与 selector 一致
的 `AuthorityStore`。这是中期可切换 PostgreSQL profile 的 runtime seam;它
不包含 authenticated service,也不会向 Agent 授予数据库访问权。

## 失败与兼容规则

- 已选择的 provider 在 selector、数据库、factory、identity 或 metadata 不可用
时,绝不回退到 File。
- `source_authority` 即使在打开失败时也标识被选择的 provider;选择未解析或
格式错误时返回 `null`。
- 选择/打开失败时 `decision_read_from_provider` 为 false,
`legacy_fallback_used` 始终为 false。
- 旧的 `openLocalAuthorityStore` 函数仍只返回 store,已有 caller 保持源码兼容。
runtime entrypoint 统一使用一个 opening seam,不再重复构造 provider。
- provider identity 只是可观测 metadata,不负责决定 Todo eligibility、claim、
lease、receipt 或 promotion。

默认 profile 是路由决策,不是迁移。已有 Markdown state、writer fence、
qualification gate 以及 File/SQLite 显式 promotion hold 均保持不变。在 shared-
authority RFC 的 D2 证据和 owner approval 完成前,SQLite 仍是 opt-in 的
qualified candidate;PostgreSQL 仍是独立的 service-provider qualification 路径。

## 验证

provider selection matrix 使用 production-scale synthetic coordination fixture
验证。测试覆盖默认 File handle、SQLite 持久化、selected provider 失败时不回退、
PostgreSQL factory identity fencing,以及 factory 返回其他 provider 时的拒绝。
File、SQLite 和 PostgreSQL 继续共享 provider-neutral transaction conformance
contract;PostgreSQL 的真实服务器 qualification 仍是独立 gate。
13 changes: 13 additions & 0 deletions loopx/control_plane/coordination/authority_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AuthorityStoreRequiredGuarantee =
(typeof AUTHORITY_STORE_REQUIRED_GUARANTEES)[number];

export type AuthorityStoreProviderKind = "file" | "nokv" | "postgresql" | "sqlite";
export type AuthorityStoreSourceAuthority = `${AuthorityStoreProviderKind}_v0`;
export type AuthorityStoreProviderStage =
| "stage1_implemented"
| "stage2a_candidate"
Expand Down Expand Up @@ -166,9 +167,21 @@ export type AuthorityStoreScanResult =

/** Storage-only seam. Legal transitions and receipt meaning stay in LoopX. */
export interface AuthorityStore {
/**
* Provider identity is observability metadata, not a semantic authority.
* Optional keeps third-party/test stores source-compatible while built-in
* providers expose an unambiguous runtime label.
*/
readonly providerKind?: AuthorityStoreProviderKind;
storeIdentity(): Promise<AuthorityStoreIdentityResult>;
loadAuthority(): Promise<AuthorityStoreLoadResult>;
commitAuthority(commit: AuthorityStoreCommit): Promise<AuthorityStoreCommitResult>;
readReceipt(operationId: string): Promise<AuthorityStoreReceiptResult>;
scanCommitted(afterCursor: string | null, limit: number): Promise<AuthorityStoreScanResult>;
}

/** Map a storage implementation to the public source label used by adapters. */
export function authorityStoreSourceAuthority(store: AuthorityStore): AuthorityStoreSourceAuthority {
const kind = store.providerKind ?? "file";
return `${kind}_v0`;
}
1 change: 1 addition & 0 deletions loopx/control_plane/coordination/file_authority_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function readFailure(error: unknown): AuthorityStoreReadFailure {

/** File-backed Stage 1 conformance provider; LoopX owns all domain decisions. */
export class FileAuthorityStore implements AuthorityStore {
readonly providerKind = "file" as const;
readonly goalId: string;
readonly directory: string;
readonly path: string;
Expand Down
Loading