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
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ Before claiming an implementation done, run the verified commands recorded
above. Never fabricate output. `.context-engine/database.env` is the generated,
ignored, mode-0600 source for local database connection contracts; `compose.yaml`
owns the pinned test service topology. A green process smoke proves only
boot/readiness. The database harness proves PG17/pgvector, role, migration, and
pool-reset infrastructure only; tenant schema, RLS enforcement, Runtime delivery,
and worker-job behavior remain `NOT_ACTIVE` until their owning issues verify them.
boot/readiness. The database harness additionally proves the bounded Organization
plus representative-record FORCE-RLS slice; Membership, the complete ActorContext
protocol, Runtime delivery, and worker-job behavior remain `NOT_ACTIVE` until their
owning issues verify them.

## Safety-Rails / Do Not

Expand Down
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
企业微信),下游把「经过授权、带证据、有预算」的 ContextPackage 交付给 agent
应用与 IM bot(飞书群聊问答优先)。

**当前状态**:M0 工程骨架已启动。API 和独立 Supply worker 可运行,真实
PostgreSQL 17 + pgvector 测试底座可复现;Runtime delivery、tenant schema、
RLS enforcement 和 worker job 行为仍为 `NOT_ACTIVE`。整体计划见
[PLAN.md](./PLAN.md)。
**当前状态**:M0 工程骨架已启动。API 和独立 Supply worker 可运行,
[`compose.yaml`](./compose.yaml) 固定的真实 PostgreSQL + pgvector 测试底座可复现;
Organization 安全根与一张代表性 tenant-owned 表的非 owner FORCE RLS 隔离切片
已验证。Runtime delivery、完整 ActorContext / Membership 以及 worker job 行为仍为
`NOT_ACTIVE`。整体计划见 [PLAN.md](./PLAN.md)。

## 开发命令

Expand All @@ -26,7 +27,7 @@ make typecheck # strict mypy
make test # 单元测试
make catalog # 安全目录静态测试与校验
make smoke # API / worker 进程 smoke
make db-up # 启动真实 PostgreSQL 17 + pgvector 测试底座
make db-up # 启动 compose.yaml 固定的 PostgreSQL + pgvector 测试底座
make db-down # 停止测试底座并保留 disposable data volume
make db-reset # 删除并重建该测试底座的 disposable data volume
make integration # 真实 PostgreSQL integration/security harness
Expand Down Expand Up @@ -71,9 +72,11 @@ uv run context-engine-worker --test-mode
```

健康响应中的 `runtime_delivery: NOT_ACTIVE` 和 worker 输出中的
`job_behavior: NOT_ACTIVE` 是能力边界。当前数据库测试只证明 PG17/pgvector、
角色隔离、空 Alembic baseline 和连接池清理契约;它没有 tenant table,也不声明
RLS、授权或 ContextPackage 交付已经实现。
`job_behavior: NOT_ACTIVE` 是能力边界。当前数据库测试证明 `compose.yaml` 固定的
PostgreSQL/pgvector、
角色隔离、迁移、连接池清理,以及 Organization + `organization_record` 的
事务级租户上下文、复合所有权和 FORCE RLS;它不声明 Membership、完整
ActorContext、Runtime 授权或 ContextPackage 交付已经实现。

本次公开候选 bundle 包含实现权威、ADR、安全契约、PRD、Tech Spec
与四个公开参考仓的证据基线;经维护者批准并提交后,它们将与实现一同
Expand All @@ -98,9 +101,11 @@ ContextEngine 的安全协议依据自身需求与威胁模型独立设计,零
- [D0 Baseline Candidate](./DESIGN-BASELINE.md):当前候选状态与尚未关闭的
evidence gates。

当前除固定 commit 的四仓静态证据与仓库内设计拆解外,已有真实 PostgreSQL 17 +
pgvector 的基础 harness 证据。tenant schema / RLS isolation、filtered ANN 和飞书
capability 的动态证据仍未完成,因此不声称这些产品能力已经验证。
当前除固定 commit 的四仓静态证据与仓库内设计拆解外,已有
[`compose.yaml`](./compose.yaml) 固定的真实 PostgreSQL + pgvector 基础 harness,
以及首个 Organization-owned 代表表的 RLS 动态证据。
完整 domain schema、ActorContext、filtered ANN 和飞书 capability 的动态证据仍未
完成,因此不把这个证据切片扩称为完整产品授权能力。

## 为什么做这个

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ global or tenant-owned. Tenant-owned entries require Organization ownership,
composite foreign keys, USING and WITH CHECK policies, FORCE RLS, and policy
tests. Coverage is computed over the manifest.

The manifest is JSON-compatible YAML by contract and is parsed with Python's
standard-library JSON decoder. YAML-only syntax is invalid. This keeps the
bootstrap and migration security gate deterministic and dependency-free, in
the same form as the canonical security catalog fixed by ADR-0019.

## Consequences

Real PostgreSQL 17 with the runtime role is a per-commit security gate.
Expand Down
84 changes: 84 additions & 0 deletions docs/decisions/0020-stage-organization-rls-before-actor-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: adr-0020-stage-organization-rls-before-actor-context
version: "1.0.0"
description: >
Bound the first Organization RLS proof without activating an incomplete
request or worker ActorContext protocol.
---

# 0020. Stage the Organization RLS proof before ActorContext activation

- Status: accepted
- Date: 2026-07-20
- Refines: ADR-0015

## Context

ADR-0015 requires a production request or worker operation to bind both its
Organization and a closed ActorContext before any content work. The first
database-isolation slice must prove Organization ownership, composite foreign
keys, non-owner FORCE RLS, transaction cleanup, and pool reuse. That slice
explicitly precedes the User, Membership, ServiceActor, WorkerLease, trusted
ingress, and sealed Runtime work needed to construct a valid ActorContext.

Inventing an opaque actor string or a temporary user table would create a
second identity protocol and make the database evidence look stronger than the
implemented trust boundary. Delaying all RLS evidence until those later domain
contracts exist would leave the selected PostgreSQL design untested.

## Decision

The first evidence slice may expose one engine-internal
`organization_transaction` boundary. It owns a fresh transaction, accepts only
an already trusted UUID value, sets `app.organization_id` transaction-locally,
verifies the readback, and only then exposes the connection. Its schema is
limited to the global Organization root and one representative tenant-owned
record.

This boundary is database evidence, not an activated request or worker
protocol. It is not exposed through HTTP or the sealed Runtime, performs no
provider, index, package, or external-effect work, and cannot raise Runtime or
worker capability status. A custom PostgreSQL setting is not authentication;
the owning ingress must establish the trusted Organization before calling this
boundary.

Before any production request or worker operation performs content work, the
owning implementation must extend the transaction protocol to bind and validate
the closed ADR-0015 ActorContext alongside Organization. UserActor construction
waits for authenticated Principal and Membership authority. ServiceActor
construction waits for the registered service identity, durable job, and signed
WorkerLease authority. No fallback actor or default Organization is permitted.

## Consequences

The repository can verify the selected Organization GUC, FORCE RLS, composite
ownership, missing-context denial, rollback, and pooled reuse against real
PostgreSQL without prematurely defining identity tables. Documentation and
capability reports must continue to mark complete ActorContext, Runtime
delivery, and worker behavior as inactive.

`DB-009`, which rejects Organization-only transactions in an activated request
or worker path, remains `NOT_ACTIVE` for this evidence-only boundary. The schema
manifest must not list that case as covered until the complete ActorContext
protocol exists and its owning negative test runs.

For the non-owner runtime role, a missing or reset Organization GUC makes the
RLS predicate unknown and therefore exposes zero tenant rows. The representative
table also has a bounded `BEFORE ... FOR EACH STATEMENT` write-context trigger;
it rejects INSERT, UPDATE, and DELETE with SQLSTATE `42501` before row filtering,
including statements for which no candidate row exists. The trigger is an
invoker-security function with a fixed `pg_catalog` search path and no PUBLIC or
worker execute grant. This prevents a missing-context write from being mistaken
for an authorized no-op. A present but different Organization remains an
ordinary RLS miss with zero visible rows and zero effects.

The representative record is an evidence carrier, not permission to generalize
an RLS framework before a second real tenant-owned domain table exists.

## Revisit trigger

Revisit when trusted ingress plus User/Membership activates the first Runtime
content transaction, or when ServiceActor plus WorkerLease activates the first
worker content transaction. That implementation must replace this staged entry
with the complete atomic Organization-and-ActorContext protocol required by
ADR-0015 and retain all existing fail-closed tests.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ touched:
- [0015 — RLS transaction context and schema manifest](0015-rls-transaction-context-and-schema-manifest.md)
- [0016 — Implementation authority and vertical-slice roadmap](0016-implementation-authority-and-vertical-slice-roadmap.md)
- [0019 — Security catalog normalization](0019-security-catalog-normalization.md)
- [0020 — Staged Organization RLS proof](0020-stage-organization-rls-before-actor-context.md)
6 changes: 6 additions & 0 deletions engine/persistence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@
)
from engine.persistence.database import create_database_engine
from engine.persistence.role_guard import assert_runtime_role
from engine.persistence.tenant_context import (
OrganizationContextBindingError,
organization_transaction,
)

__all__ = [
"DatabaseConfiguration",
"DatabaseConfigurationError",
"DatabasePurpose",
"HarnessDatabaseConfigurations",
"OrganizationContextBindingError",
"assert_runtime_role",
"create_database_engine",
"load_database_configuration",
"load_harness_database_configurations",
"organization_transaction",
]
98 changes: 98 additions & 0 deletions engine/persistence/schema_security_manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"manifestVersion": "1.0.0",
"tables": [
{
"name": "alembic_version",
"classification": "global",
"purpose": "Alembic migration metadata",
"partitions": []
},
{
"name": "organization",
"classification": "global",
"purpose": "Organization security root",
"primaryKey": {
"name": "pk_organization",
"columns": ["organization_id"]
},
"permittedOperations": {
"context_engine_runtime": [],
"context_engine_worker": []
},
"partitions": []
},
{
"name": "organization_record",
"classification": "tenant_owned",
"purpose": "Representative tenant-owned record for the first isolation slice",
"organizationColumn": "organization_id",
"organizationInclusiveKeys": [
{
"name": "pk_organization_record",
"kind": "primary_key",
"columns": ["organization_id", "record_id"]
}
],
"foreignKeys": [
{
"name": "fk_organization_record_organization",
"columns": ["organization_id"],
"references": {
"table": "organization",
"columns": ["organization_id"]
}
},
{
"name": "fk_organization_record_parent_same_organization",
"columns": ["organization_id", "parent_record_id"],
"references": {
"table": "organization_record",
"columns": ["organization_id", "record_id"]
}
}
],
"rowLevelSecurity": {
"enabled": true,
"forced": true,
"policies": [
{
"name": "organization_record_organization_isolation",
"command": "ALL",
"roles": ["context_engine_runtime"],
"using": "organization_id = NULLIF(current_setting('app.organization_id', true), '')::uuid",
"withCheck": "organization_id = NULLIF(current_setting('app.organization_id', true), '')::uuid"
}
],
"writeContextGuard": {
"trigger": "organization_record_write_context_guard",
"function": "organization_record_require_write_context",
"timing": "BEFORE",
"orientation": "STATEMENT",
"events": ["INSERT", "UPDATE", "DELETE"],
"missingContextSqlstate": "42501"
}
},
"permittedOperations": {
"context_engine_runtime": ["SELECT", "INSERT", "UPDATE", "DELETE"],
"context_engine_worker": []
},
"partitions": [],
"securityInvariantIds": [
"TENANT-OWNERSHIP-001",
"TENANT-FK-002",
"RLS-FAIL-CLOSED-003"
],
"negativeTestIds": [
"DB-001",
"DB-002",
"DB-003",
"DB-004",
"DB-006",
"DB-007",
"DB-008",
"MIG-001",
"MIG-002"
]
}
]
}
53 changes: 53 additions & 0 deletions engine/persistence/tenant_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Transaction-local Organization context for the first RLS evidence slice.

The Organization UUID passed here must already have been established by a
trusted caller. This database boundary deliberately does not authenticate a
request or manufacture the future closed ActorContext contract.
"""

from __future__ import annotations

from collections.abc import Iterator
from contextlib import contextmanager
from uuid import UUID

from sqlalchemy import Connection, Engine, text


class OrganizationContextBindingError(RuntimeError):
"""Raised before content work when PostgreSQL did not retain the local GUC."""


@contextmanager
def organization_transaction(
engine: Engine,
organization_id: UUID,
) -> Iterator[Connection]:
"""Open, bind, verify, and own one Organization-scoped transaction.

No connection is exposed before ``app.organization_id`` is set with
transaction-local scope. Normal exit commits; every exception rolls the
transaction back through SQLAlchemy's engine context manager.
"""

if not isinstance(organization_id, UUID):
raise TypeError("organization_id must be a UUID")

expected_value = str(organization_id)
with engine.begin() as connection:
connection.execute(
text(
"SELECT set_config("
"'app.organization_id', :organization_id, true"
")"
),
{"organization_id": expected_value},
)
observed_value = connection.execute(
text("SELECT current_setting('app.organization_id', true)")
).scalar_one()
if observed_value != expected_value:
raise OrganizationContextBindingError(
"organization context binding failed before database work"
)
yield connection
Loading
Loading