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
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +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 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.
boot/readiness. The database harness additionally proves Organization, current
Membership, the online UserActor transaction, and representative-record FORCE-RLS
isolation. Content-bearing Runtime delivery and worker-job behavior remain
`NOT_ACTIVE` until their owning issues verify them.

## Safety-Rails / Do Not

Expand Down
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

**当前状态**:M0 工程骨架已启动。API 和独立 Supply worker 可运行,
[`compose.yaml`](./compose.yaml) 固定的真实 PostgreSQL + pgvector 测试底座可复现;
Organization 安全根与一张代表性 tenant-owned 表的非 owner FORCE RLS 隔离切片
已验证;HTTP 已能用确定性测试认证构造 nominal `AuthenticatedInvocation`,并用
Organization 安全根、全局 User、Organization-scoped Membership 与一张代表性
tenant-owned 表的非 owner FORCE RLS 隔离已验证;HTTP 已能把确定性测试认证解析成
当前 Membership-backed `UserActor`,构造 nominal `AuthenticatedInvocation`,并用
closed body 与通用错误证明 caller 不能注入 trusted identity;该测试组合已通过唯一
`ContextRuntime.resolve` 返回 tenant-safe、evidence-free ContextPackage。默认应用仍拒绝全部
credential;生产认证、带内容 Runtime delivery、完整 ActorContext / Membership 以及
worker job 行为仍为 `NOT_ACTIVE`。整体计划见 [PLAN.md](./PLAN.md)。
credential;生产认证、带内容 Runtime delivery、Principal/Agent scope 交集以及 worker job
行为仍为 `NOT_ACTIVE`。整体计划见 [PLAN.md](./PLAN.md)。

## 开发命令

Expand Down Expand Up @@ -77,20 +78,21 @@ uv run context-engine-worker --test-mode
健康响应中的 `runtime_delivery: NOT_ACTIVE` 表示默认进程没有生产认证入口,worker 输出中的
`job_behavior: NOT_ACTIVE` 是能力边界。当前数据库测试证明 `compose.yaml` 固定的
PostgreSQL/pgvector、
角色隔离、迁移、连接池清理,以及 Organization + `organization_record` 的
事务级租户上下文、复合所有权和 FORCE RLS;它不声明 Membership、完整
ActorContext、内容授权或生产 ContextPackage 交付已经实现;注入的 conformance 组合只证明
Issue #10 的安全空包路径
角色隔离、迁移、连接池清理,以及 Organization + current Membership-backed
`UserActor` + `organization_record` 的事务级租户上下文、复合所有权和 FORCE RLS
它不声明 Principal/Agent scope、内容授权或生产 ContextPackage 交付已经实现;注入的
conformance 组合证明 Issue #11 在安全空包路径上增加的当前 Membership 门禁

### 当前 HTTP empty-Package tracer

`POST /v1/context:resolve` 的 conformance 组合可注入一个把 opaque credential
映射为 verified transport facts 的 authenticator,以及一个为已登记 Organization
签发 request-bound nominal proof 的 trusted authority;ingress 构造 nominal
`AuthenticatedInvocation` 与 server-owned direct `TrustedDeliveryContext` 后,恰好
调用一次 sealed Runtime。有效 Acquire 返回 `200 resolved` 与 evidence-free
ContextPackage;模块级默认应用的两条 authority 均 reject-all,因此不会接受任何
生产 credential。
映射为 verified transport facts 的 authenticator、一个为已登记 Organization
签发 request-bound nominal proof 的 trusted authority,以及一个在单次 PostgreSQL
事务内校验 current Membership 并签发 lifetime-bound `UserActor` proof 的 authority;
该事务保持到 sealed Runtime 与 ContextPackage 构造完成。有效 Acquire 返回
`200 resolved` 与 evidence-free ContextPackage;无效 Membership 统一返回通用 401,
数据库 authority 不可用统一返回通用 503,且两者都不会调用内容系统。模块级默认应用的
三条 authority 均 reject-all,因此不会接受任何生产 credential。

请求体仅允许 `kind: "acquire"`、`need.query`、可选的有限 `packageBudget` 和可选的
`requestNarrowing`(ref 长度与集合数量均受 active profile 限制),每层 unknown field、重复 JSON key
Expand All @@ -101,9 +103,9 @@ JSON/media type、
不会回显 tenant、Principal、Membership 或注入字段。purpose 只来自服务端 route
policy;返回的 `organizationRef` 是新生成的 package-scoped opaque reference,不能作为
后续请求的 trusted tenant input。空包的 blocks/evidence/gaps 均为空,coverage 为
`no_authorized_evidence`,Provider/index/source-content 调用均为零。确定性与
real-PostgreSQL seeded Organization authority 只属于测试组合。生产 OAuth/JWT、
Membership 查询、ActorContext、Evidence 与 continuation 不属于这个已激活 tracer。
`no_authorized_evidence`,Provider/index/source-content 调用均为零。确定性 authorities
real-PostgreSQL seeded composition 只属于测试组合。生产 OAuth/JWT、Principal/
Agent scope、Evidence 与 continuation 不属于这个已激活 tracer。

本次公开候选 bundle 包含实现权威、ADR、安全契约、PRD、Tech Spec
与四个公开参考仓的证据基线;经维护者批准并提交后,它们将与实现一同
Expand Down
143 changes: 109 additions & 34 deletions adapters/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import UTC, datetime
from json import loads
from typing import Annotated, Final, Literal, cast
from uuid import uuid4
from uuid import UUID, uuid4

from fastapi import Depends, FastAPI, Header, Request, Response, Security
from fastapi.exception_handlers import http_exception_handler
Expand All @@ -28,6 +28,11 @@
CoverageWire,
InvalidRequestWire,
ResolvedWire,
ServiceUnavailableWire,
)
from adapters.http.membership_authority import (
MembershipAuthority,
RejectingMembershipAuthority,
)
from adapters.http.organization_authority import (
OrganizationAuthority,
Expand All @@ -41,7 +46,13 @@
enforce_json_nesting,
)
from engine import BUILD_IDENTIFIER
from engine.persistence.membership_context import (
MembershipAuthorityUnavailable,
MembershipIdentity,
MembershipNotCurrent,
)
from engine.runtime import AuthenticatedInvocation, Runtime
from engine.runtime.actor import MembershipRejectionAuditReceipt
from engine.runtime.budget import PackageBudgetRequest
from engine.runtime.construction import required_kernel_dependencies
from engine.runtime.contracts import Acquire, ContextNeed, RequestNarrowing, Resolved
Expand All @@ -56,13 +67,18 @@
}
AUTHENTICATION_FAILED_RESPONSE: Final = {"code": "authentication_failed"}
INVALID_REQUEST_RESPONSE: Final = {"code": "invalid_request"}
SERVICE_UNAVAILABLE_RESPONSE: Final = {"code": "service_unavailable"}
RESOLVE_PATH: Final = "/v1/context:resolve"


class TransportAuthenticationFailed(Exception):
"""Authentication failed without exposing credential or identity detail."""


class TrustedAuthorityUnavailable(Exception):
"""A required trusted authority failed without exposing identity detail."""


class InvalidRequestMediaType(Exception):
"""Resolve received a body outside its sole JSON media type."""

Expand Down Expand Up @@ -105,9 +121,13 @@ def create_app(
*,
authenticator: Authenticator | None = None,
organization_authority: OrganizationAuthority | None = None,
membership_authority: MembershipAuthority | None = None,
runtime: Runtime | None = None,
invocation_observer: Callable[[AuthenticatedInvocation], None] | None = None,
resolution_observer: Callable[[Resolved], None] | None = None,
membership_rejection_observer: (
Callable[[MembershipRejectionAuditReceipt], None] | None
) = None,
clock: Callable[[], datetime] = _utc_now,
request_id_factory: Callable[[], str] = _new_request_id,
transport_profile: HttpTransportProfile = HTTP_TRANSPORT_PROFILE_V1,
Expand All @@ -124,6 +144,9 @@ def create_app(
selected_organization_authority = (
organization_authority or RejectingOrganizationAuthority()
)
selected_membership_authority = (
membership_authority or RejectingMembershipAuthority()
)
bearer = HTTPBearer(
scheme_name="ContextEngineBearer",
bearerFormat="opaque",
Expand All @@ -148,6 +171,14 @@ async def authentication_failed(
headers={"WWW-Authenticate": "Bearer"},
)

@app.exception_handler(TrustedAuthorityUnavailable)
async def trusted_authority_unavailable(
request: Request,
error: TrustedAuthorityUnavailable,
) -> JSONResponse:
del request, error
return JSONResponse(SERVICE_UNAVAILABLE_RESPONSE, status_code=503)

@app.exception_handler(InvalidRequestMediaType)
@app.exception_handler(InvalidJsonTransport)
async def invalid_media_type(
Expand Down Expand Up @@ -251,6 +282,10 @@ def health() -> dict[str, str]:
"model": InvalidRequestWire,
"description": "The closed request schema rejected the body.",
},
503: {
"model": ServiceUnavailableWire,
"description": "A required trusted authority is unavailable.",
},
},
)
def resolve_context(
Expand Down Expand Up @@ -281,46 +316,86 @@ def resolve_context(
verified_at=received_at,
)
)
invocation = _construct_authenticated_http_invocation(
request_id=request_id,
authenticated_organization_ref=authentication.organization_ref,
organization_verification=organization_verification,
membership_identity = MembershipIdentity(
organization_id=UUID(authentication.organization_ref),
user_id=UUID(authentication.user_ref),
membership_id=UUID(authentication.membership_ref),
membership_version=authentication.membership_version,
principal_ref=authentication.principal_ref,
membership_ref=authentication.membership_ref,
agent_version_ref=authentication.agent_version_ref,
authenticated_application_ref=(
authentication.authenticated_application_ref
),
request_id=request_id,
authentication_binding_ref=(
authentication.authentication_binding_ref
),
received_at=received_at,
checked_at=received_at,
)
except (OrganizationVerificationRejected, TypeError, ValueError):
raise TransportAuthenticationFailed from None
if invocation_observer is not None:
invocation_observer(invocation)
delivery_context = _construct_direct_delivery_context(
purpose=DIRECT_ACQUIRE_PURPOSE,
authenticated_application_ref=(
authentication.authenticated_application_ref
),
delivery_binding_ref=authentication.authentication_binding_ref,
established_at=invocation.received_at,
)
request = _acquire_from_wire(body)
outcome = selected_runtime.resolve(invocation, delivery_context, request)
response = _resolved_to_wire(outcome)
if resolution_observer is not None:
resolution_observer(outcome)
return JSONResponse(
response.model_dump(mode="json", by_alias=True),
status_code=200,
headers={
"Cache-Control": "no-store",
"X-Context-Request-Id": invocation.request_id,
},
)
try:
with selected_membership_authority.current_user_actor(
membership_identity
) as current_membership_verification:
try:
invocation = _construct_authenticated_http_invocation(
request_id=request_id,
authenticated_organization_ref=(
authentication.organization_ref
),
organization_verification=organization_verification,
user_ref=authentication.user_ref,
principal_ref=authentication.principal_ref,
membership_ref=authentication.membership_ref,
membership_version=authentication.membership_version,
current_membership_verification=(
current_membership_verification
),
agent_version_ref=authentication.agent_version_ref,
authenticated_application_ref=(
authentication.authenticated_application_ref
),
authentication_binding_ref=(
authentication.authentication_binding_ref
),
received_at=received_at,
)
except (TypeError, ValueError):
raise TransportAuthenticationFailed from None
if invocation_observer is not None:
invocation_observer(invocation)
delivery_context = _construct_direct_delivery_context(
purpose=DIRECT_ACQUIRE_PURPOSE,
authenticated_application_ref=(
authentication.authenticated_application_ref
),
delivery_binding_ref=(
authentication.authentication_binding_ref
),
established_at=invocation.received_at,
)
request = _acquire_from_wire(body)
outcome = selected_runtime.resolve(
invocation,
delivery_context,
request,
)
response = _resolved_to_wire(outcome)
if resolution_observer is not None:
resolution_observer(outcome)
return JSONResponse(
response.model_dump(mode="json", by_alias=True),
status_code=200,
headers={
"Cache-Control": "no-store",
"X-Context-Request-Id": invocation.request_id,
},
)
except MembershipNotCurrent as error:
if type(error) is not MembershipNotCurrent:
raise TrustedAuthorityUnavailable from None
if membership_rejection_observer is not None:
membership_rejection_observer(error.audit_receipt)
raise TransportAuthenticationFailed from None
except MembershipAuthorityUnavailable:
raise TrustedAuthorityUnavailable from None

return app

Expand Down
35 changes: 22 additions & 13 deletions adapters/http/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Protocol
from uuid import UUID

from engine.runtime.actor import MAX_MEMBERSHIP_VERSION


class AuthenticationRejected(Exception):
"""Opaque credential did not establish verified authentication context."""
Expand All @@ -18,16 +20,20 @@ class VerifiedAuthenticationContext:
"""Identity facts emitted by a verified transport/session authenticator."""

organization_ref: str
user_ref: str
principal_ref: str
membership_ref: str | None
membership_ref: str
membership_version: int
agent_version_ref: str
authenticated_application_ref: str
authentication_binding_ref: str

def __post_init__(self) -> None:
required_refs = (
self.organization_ref,
self.user_ref,
self.principal_ref,
self.membership_ref,
self.agent_version_ref,
self.authenticated_application_ref,
self.authentication_binding_ref,
Expand All @@ -39,20 +45,23 @@ def __post_init__(self) -> None:
raise InvalidAuthenticationContext(
"verified authentication refs must be non-empty"
)
try:
organization_id = UUID(self.organization_ref)
except ValueError:
raise InvalidAuthenticationContext(
"verified Organization ref must be an internal UUID"
) from None
object.__setattr__(self, "organization_ref", str(organization_id))
if self.membership_ref is not None and (
type(self.membership_ref) is not str
or not self.membership_ref
or self.membership_ref.isspace()
for field_name in ("organization_ref", "user_ref", "membership_ref"):
value = getattr(self, field_name)
try:
internal_id = UUID(value)
except ValueError:
label = field_name.removesuffix("_ref").replace("_", " ")
raise InvalidAuthenticationContext(
f"verified {label} ref must be an internal UUID"
) from None
object.__setattr__(self, field_name, str(internal_id))
if (
type(self.membership_version) is not int
or not 1 <= self.membership_version <= MAX_MEMBERSHIP_VERSION
):
raise InvalidAuthenticationContext(
"verified membership ref must be non-empty"
"verified Membership version must fit a positive signed 64-bit "
"integer"
)


Expand Down
6 changes: 6 additions & 0 deletions adapters/http/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,9 @@ class InvalidRequestWire(ClosedWireModel):
"""Closed public response for request syntax or schema rejection."""

code: Literal["invalid_request"]


class ServiceUnavailableWire(ClosedWireModel):
"""Closed response when a required trusted authority is unavailable."""

code: Literal["service_unavailable"]
Loading
Loading