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
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ and evidence-based verification as the generator and runtimes evolve.

* **Python**: 3.10+ (Recommended 3.12+)
* **Testing**: `pytest` 7.4+
* **Dependencies**: `pydantic` >= 2.0, `aiosqlite`, `aiomysql`, `asyncpg`
* **Dependencies**: `pydantic` >= 2.0, `aiosqlite`, `aiomysql`, `asyncpg`, `cryptography` >= 42

## 2. Tests Performed

Expand Down Expand Up @@ -96,19 +96,41 @@ non-empty comment/purpose and audited mutations retain their audit reason.
Runtime logging should keep parameterized SQL and intent separate from any
restricted value-bearing diagnostic output.

Portable `UserContext` opaque entity references are not implemented in the
Python runtime yet. Until that capability is added, applications must not
invent a Python-specific token format or serialize internal ID/version pairs as
if they were the TeaQL portable contract. A Python TFP client may carry an
opaque token issued by a trusted Java, Rust, Go, or .NET backend, but it must
not decode, rewrite, or mint that token.
Python implements the portable `tqr1` tuple codec shared with Go and .NET. It
encrypts and authenticates entity type, internal ID, optimistic version,
issued/expiry time, and purpose with AES-256-GCM. Key rings permit rotation;
encoding always uses the active key while decoding can accept retained keys.

The planned wire format, fail-closed behavior, shared golden vector, and exact
```python
import os
from datetime import timedelta
from teaql.runtime import AeadEntityReferenceCodec, UserContext

codec = AeadEntityReferenceCodec(2, {
1: bytes.fromhex(os.environ["TEAQL_ENTITY_REFERENCE_KEY_V1_HEX"]),
2: bytes.fromhex(os.environ["TEAQL_ENTITY_REFERENCE_KEY_V2_HEX"]),
})
context = UserContext().with_entity_reference_codec(codec)
token = context.encode_entity_reference(
"OrderItem", 42, 7, "edit-order", timedelta(minutes=30)
)
claims = context.decode_entity_reference(token, "OrderItem", "edit-order")
```

Without a configured codec the runtime fails closed. Local debugging can use
the canonical long `TEAQL_UNSAFE_RAW_ENTITY_REFERENCES` acknowledgement, which
emits visibly distinct `tqr0` references. It must not be enabled in production.

The wire format, fail-closed behavior, shared golden vector, and exact
development-only acknowledgement are maintained in the canonical
[opaque entity reference contract](https://github.com/teaql/teaql-conformance/blob/main/design/opaque-entity-references.md).
Opaque tokens never replace the backend's authorization, tenant, ownership,
role, or optimistic-version checks.

The repeatable [`examples/opaque-entity-reference`](examples/opaque-entity-reference)
example proves the exact cross-language golden vector and purpose-substitution
rejection.

### Mutation Policy installation

Policy implementations are installed from trusted application startup through
Expand Down
6 changes: 6 additions & 0 deletions examples/opaque-entity-reference/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Opaque entity reference

This example proves that Python emits the same portable `tqr1` golden vector as
Go and .NET, decodes it through `UserContext`, and rejects purpose
substitution. Decoding authenticates identity claims; it does not replace the
application's ownership, permission, or optimistic-version checks.
26 changes: 26 additions & 0 deletions examples/opaque-entity-reference/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from datetime import datetime, timedelta, timezone

from teaql.runtime import AeadEntityReferenceCodec, EntityReferenceTokenError, UserContext


GOLDEN = "tqr1.AAAAAjMzMzMzMzMzMzMzM3bKiZgRSQQhfIj2cBXRDZIloUGHWLBp8QrXL_aejwIXPFtvV_E71O7wbOXy3cvYo_SwxvuS-89x572T9CO_pDAY4tbjWCNv"
now = datetime(2026, 9, 26, 12, 0, tzinfo=timezone.utc)
codec = AeadEntityReferenceCodec(
2, {1: bytes([0x11]) * 32, 2: bytes([0x22]) * 32}
).with_clock(lambda: now).with_nonce_source(lambda: bytes([0x33]) * 12)
context = UserContext().with_entity_reference_codec(codec)

token = context.encode_entity_reference(
"OrderItem", 42, 7, "edit-order", timedelta(hours=1)
)
assert token == GOLDEN
claims = context.decode_entity_reference(token, "OrderItem", "edit-order")
assert (claims.id, claims.version, claims.key_version) == (42, 7, 2)

try:
context.decode_entity_reference(token, "OrderItem", "view-order")
raise AssertionError("purpose substitution must fail")
except EntityReferenceTokenError as error:
assert error.code == "ENTITY_REFERENCE_INVALID"

print("PASS: Python opaque entity reference")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"aiomysql>=0.2.0",
"asyncpg>=0.29.0",
"httpx>=0.24.0",
"cryptography>=42.0.0",
]

[project.optional-dependencies]
Expand Down
3 changes: 2 additions & 1 deletion scripts/verify-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
expected=(business-clock conformance mutation-policy order-management query-policy school-management task_board)
expected=(business-clock conformance mutation-policy opaque-entity-reference order-management query-policy school-management task_board)
mapfile -t actual < <(find "$repo/examples" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
if [[ "${actual[*]}" != "${expected[*]}" ]]; then
echo "example inventory changed; update scripts/verify-examples.sh: ${actual[*]}" >&2
Expand All @@ -27,6 +27,7 @@ PYTHONPATH="$repo/src" python -m unittest discover -s "$repo/examples/school-man
PYTHONPATH="$repo/src" python "$repo/examples/mutation-policy/main.py"
PYTHONPATH="$repo/src" python "$repo/examples/business-clock/main.py"
PYTHONPATH="$repo/src" python "$repo/examples/query-policy/main.py"
PYTHONPATH="$repo/src" python "$repo/examples/opaque-entity-reference/main.py"
order_management_tmp="$(mktemp -d)"
task_board_tmp="$(mktemp -d)"
trap 'rm -rf "$order_management_tmp" "$task_board_tmp"' EXIT
Expand Down
11 changes: 10 additions & 1 deletion src/teaql/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
from .store import DataStore
from .audit import RawAuditEvent, SafeAuditEvent, MutationAuditKind
from .business_clock import BusinessClock, FixedBusinessClock, SystemBusinessClock
from .entity_reference import (
AeadEntityReferenceCodec,
EntityReferenceClaims,
EntityReferenceCodec,
EntityReferenceTokenError,
ENTITY_REFERENCE_AAD,
UNSAFE_RAW_ENTITY_REFERENCES_ACKNOWLEDGEMENT,
UNSAFE_RAW_ENTITY_REFERENCES_ENVIRONMENT,
)
from .mutation_policy import (
MISSING_APPROVAL,
MISSING_POLICY,
Expand All @@ -35,7 +44,7 @@
from .wire_fields import NormalizedWireInput, WireEntityMetadata, WireFieldMetadata, WireInputError, create_wire_entity_metadata, encode_wire_output, normalize_wire_input, retain_submitted_paths
from teaql.core.entity import EntityKey, EntityChangeSet, EntityRoot

__all__ = ["WireFieldMetadata", "WireEntityMetadata", "NormalizedWireInput", "WireInputError", "create_wire_entity_metadata", "normalize_wire_input", "encode_wire_output", "retain_submitted_paths", "EntityKey", "EntityChangeSet", "EntityRoot", "ContextEntityRef", "ContextRootError", "CheckException", "CheckResult", "I18nCatalog", "JsonFieldNamingProfile", "Locale", "ObjectLocation", "UnsupportedLocaleError", "UserContext", "TeaqlRuntime", "SqlLogEntry", "SqlLogOperation", "DiagnosticSqlLogSink", "TextDiagnosticSqlLogSink", "ServiceRuntimeFromEnv", "RuntimeModule", "DataStore", "RawAuditEvent", "SafeAuditEvent", "MutationAuditKind", "BusinessClock", "FixedBusinessClock", "SystemBusinessClock", "ContextTools", "ExecutableHttpTool", "HTTP_TOOL", "HttpIntentPhase", "HttpTool", "HttpToolProvider", "ToolDeniedError", "ToolError", "ToolPolicy", "ToolRisk", "Tools", "ToolToken", "ToolUnavailableError"]
__all__ = ["WireFieldMetadata", "WireEntityMetadata", "NormalizedWireInput", "WireInputError", "create_wire_entity_metadata", "normalize_wire_input", "encode_wire_output", "retain_submitted_paths", "EntityKey", "EntityChangeSet", "EntityRoot", "ContextEntityRef", "ContextRootError", "CheckException", "CheckResult", "I18nCatalog", "JsonFieldNamingProfile", "Locale", "ObjectLocation", "UnsupportedLocaleError", "UserContext", "TeaqlRuntime", "SqlLogEntry", "SqlLogOperation", "DiagnosticSqlLogSink", "TextDiagnosticSqlLogSink", "ServiceRuntimeFromEnv", "RuntimeModule", "DataStore", "RawAuditEvent", "SafeAuditEvent", "MutationAuditKind", "BusinessClock", "FixedBusinessClock", "SystemBusinessClock", "AeadEntityReferenceCodec", "EntityReferenceClaims", "EntityReferenceCodec", "EntityReferenceTokenError", "ENTITY_REFERENCE_AAD", "UNSAFE_RAW_ENTITY_REFERENCES_ACKNOWLEDGEMENT", "UNSAFE_RAW_ENTITY_REFERENCES_ENVIRONMENT", "ContextTools", "ExecutableHttpTool", "HTTP_TOOL", "HttpIntentPhase", "HttpTool", "HttpToolProvider", "ToolDeniedError", "ToolError", "ToolPolicy", "ToolRisk", "Tools", "ToolToken", "ToolUnavailableError"]

__all__ += [
"MISSING_APPROVAL", "MISSING_POLICY",
Expand Down
58 changes: 57 additions & 1 deletion src/teaql/runtime/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
from copy import deepcopy
from dataclasses import dataclass
from array import array
from datetime import date, datetime
from datetime import date, datetime, timedelta, timezone

from .business_clock import BusinessClock, SystemBusinessClock
from .entity_reference import (
EntityReferenceClaims,
EntityReferenceCodec,
EntityReferenceTokenError,
decode_raw_entity_reference,
encode_raw_entity_reference,
raw_entity_references_enabled,
validate_decoded_reference,
validate_reference_request,
)


TEntity = TypeVar("TEntity")
Expand Down Expand Up @@ -110,6 +120,7 @@ def __init__(self):
self._fix_evidence_last: List[FixEvidence] = []
self._checked_mutations = set()
self._business_clock: BusinessClock = SystemBusinessClock()
self._entity_reference_codec: Optional[EntityReferenceCodec] = None
from .mutation_policy import MutationPolicyRuntimeState
self._mutation_policy = MutationPolicyRuntimeState()

Expand Down Expand Up @@ -151,6 +162,51 @@ def require_active_root(self, expected_type: str) -> ContextEntityRef:
raise ContextRootError("type_mismatch", expected_type, root)
return root

def with_entity_reference_codec(
self, codec: EntityReferenceCodec
) -> 'UserContext':
if codec is None:
raise TypeError("entity reference codec is required")
self._entity_reference_codec = codec
return self

def encode_entity_reference(
self,
entity_type: str,
entity_id: int,
version: int,
purpose: str,
lifetime: timedelta,
) -> str:
if self._entity_reference_codec is not None:
return self._entity_reference_codec.encode_entity_reference(
entity_type, entity_id, version, purpose, lifetime
)
if not raw_entity_references_enabled():
raise EntityReferenceTokenError("ENTITY_REFERENCE_CODEC_REQUIRED")
validate_reference_request(entity_type, entity_id, version, purpose, lifetime)
now = datetime.now(timezone.utc)
return encode_raw_entity_reference(
EntityReferenceClaims(
entity_type, entity_id, version, now, now + lifetime, purpose
)
)

def decode_entity_reference(
self, token: str, expected_entity_type: str, purpose: str
) -> EntityReferenceClaims:
if self._entity_reference_codec is not None:
return self._entity_reference_codec.decode_entity_reference(
token, expected_entity_type, purpose
)
if not raw_entity_references_enabled():
raise EntityReferenceTokenError("ENTITY_REFERENCE_CODEC_REQUIRED")
claims = decode_raw_entity_reference(token)
validate_decoded_reference(
claims, datetime.now(timezone.utc), expected_entity_type, purpose
)
return claims

async def execute_graph_save(self, work):
"""Run one generated entity graph in one provider transaction."""
if self._graph_save_owner.get() is not None:
Expand Down
Loading
Loading