generated from coulomb/repo-seed
feat(memory): add permission aware retrieval
This commit is contained in:
@@ -22,6 +22,10 @@ with `markitect-tool`.
|
|||||||
tests and future service wiring.
|
tests and future service wiring.
|
||||||
- `MemoryRuntimeService.import_markitect_graph()` persists an imported graph and
|
- `MemoryRuntimeService.import_markitect_graph()` persists an imported graph and
|
||||||
can attach an audit event when an `OperationContext` is supplied.
|
can attach an audit event when an `OperationContext` is supplied.
|
||||||
|
- `MemoryRuntimeService.query_memory()` retrieves graph nodes through a scope
|
||||||
|
policy check plus per-node policy checks, returns source-grounded context
|
||||||
|
items, preserves safe denied diagnostics, and emits an audit event in the
|
||||||
|
result envelope.
|
||||||
|
|
||||||
## Boundary
|
## Boundary
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ from .services import (
|
|||||||
ContextEntityQueryResult,
|
ContextEntityQueryResult,
|
||||||
LexicalIndexRefreshResult,
|
LexicalIndexRefreshResult,
|
||||||
MemoryGraphImportSummary,
|
MemoryGraphImportSummary,
|
||||||
|
MemoryQueryRequest,
|
||||||
|
MemoryRetrievalItem,
|
||||||
|
MemoryRetrievalResult,
|
||||||
MemoryRuntimeService,
|
MemoryRuntimeService,
|
||||||
RelationshipChangeResult,
|
RelationshipChangeResult,
|
||||||
RelationshipQueryItem,
|
RelationshipQueryItem,
|
||||||
@@ -261,6 +264,9 @@ __all__ = [
|
|||||||
"MemoryGraphRepository",
|
"MemoryGraphRepository",
|
||||||
"MemoryNodeRecord",
|
"MemoryNodeRecord",
|
||||||
"MemoryProfileRecord",
|
"MemoryProfileRecord",
|
||||||
|
"MemoryQueryRequest",
|
||||||
|
"MemoryRetrievalItem",
|
||||||
|
"MemoryRetrievalResult",
|
||||||
"MemoryRuntimeService",
|
"MemoryRuntimeService",
|
||||||
"MemorySourceSpan",
|
"MemorySourceSpan",
|
||||||
"NormalizedDocument",
|
"NormalizedDocument",
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ from .asset_service import (
|
|||||||
)
|
)
|
||||||
from .content_service import RepresentationContentResult, RepresentationContentStream, RepresentationContentService
|
from .content_service import RepresentationContentResult, RepresentationContentStream, RepresentationContentService
|
||||||
from .ingestion_service import AssetIngestionResult, AssetIngestionService
|
from .ingestion_service import AssetIngestionResult, AssetIngestionService
|
||||||
from .memory_service import MemoryGraphImportSummary, MemoryRuntimeService
|
from .memory_service import (
|
||||||
|
MemoryGraphImportSummary,
|
||||||
|
MemoryQueryRequest,
|
||||||
|
MemoryRetrievalItem,
|
||||||
|
MemoryRetrievalResult,
|
||||||
|
MemoryRuntimeService,
|
||||||
|
)
|
||||||
from .retrieval_service import (
|
from .retrieval_service import (
|
||||||
AssetQueryItem,
|
AssetQueryItem,
|
||||||
AssetQueryRequest,
|
AssetQueryRequest,
|
||||||
@@ -55,6 +61,9 @@ __all__ = [
|
|||||||
"ContextEntityQueryResult",
|
"ContextEntityQueryResult",
|
||||||
"LexicalIndexRefreshResult",
|
"LexicalIndexRefreshResult",
|
||||||
"MemoryGraphImportSummary",
|
"MemoryGraphImportSummary",
|
||||||
|
"MemoryQueryRequest",
|
||||||
|
"MemoryRetrievalItem",
|
||||||
|
"MemoryRetrievalResult",
|
||||||
"MemoryRuntimeService",
|
"MemoryRuntimeService",
|
||||||
"RelationshipChangeResult",
|
"RelationshipChangeResult",
|
||||||
"RepresentationContentResult",
|
"RepresentationContentResult",
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ from kontextual_engine.core import (
|
|||||||
AuditEvent,
|
AuditEvent,
|
||||||
AuditOutcome,
|
AuditOutcome,
|
||||||
MemoryGraphImportResult,
|
MemoryGraphImportResult,
|
||||||
|
MemoryEdgeRecord,
|
||||||
|
MemoryNodeRecord,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
|
PolicyDecision,
|
||||||
)
|
)
|
||||||
from kontextual_engine.errors import ValidationError
|
from kontextual_engine.errors import Diagnostic, ValidationError
|
||||||
from kontextual_engine.ports import MemoryGraphRepository
|
from kontextual_engine.ports import AllowAllPolicyGateway, MemoryGraphRepository, PolicyGateway
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -41,9 +44,118 @@ class MemoryGraphImportSummary:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MemoryQueryRequest:
|
||||||
|
graph_id: str | None = None
|
||||||
|
node_ids: tuple[str, ...] = ()
|
||||||
|
kinds: tuple[str, ...] = ()
|
||||||
|
text_contains: str | None = None
|
||||||
|
include_edges: bool = True
|
||||||
|
limit: int = 50
|
||||||
|
offset: int = 0
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "node_ids", tuple(self.node_ids))
|
||||||
|
object.__setattr__(self, "kinds", tuple(self.kinds))
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"graph_id": self.graph_id,
|
||||||
|
"node_ids": list(self.node_ids),
|
||||||
|
"kinds": list(self.kinds),
|
||||||
|
"text_contains": self.text_contains,
|
||||||
|
"include_edges": self.include_edges,
|
||||||
|
"limit": self.limit,
|
||||||
|
"offset": self.offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MemoryRetrievalItem:
|
||||||
|
node: MemoryNodeRecord
|
||||||
|
edges: tuple[MemoryEdgeRecord, ...] = ()
|
||||||
|
policy_decision: PolicyDecision | None = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "edges", tuple(self.edges))
|
||||||
|
|
||||||
|
def to_context_item(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"node_id": self.node.node_id,
|
||||||
|
"contract_node_id": self.node.contract_node_id,
|
||||||
|
"kind": self.node.kind,
|
||||||
|
"text": self.node.text,
|
||||||
|
"source_spans": [span.to_dict() for span in self.node.source_spans],
|
||||||
|
"provenance": [dict(item) for item in self.node.provenance],
|
||||||
|
"metadata": {
|
||||||
|
"graph_id": self.node.graph_id,
|
||||||
|
"contract_graph_id": self.node.contract_graph_id,
|
||||||
|
"edges": [edge.to_dict() for edge in self.edges],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"node": self.node.to_dict(),
|
||||||
|
"edges": [edge.to_dict() for edge in self.edges],
|
||||||
|
"context_item": self.to_context_item(),
|
||||||
|
"policy_decision": self.policy_decision.to_dict() if self.policy_decision else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MemoryRetrievalResult:
|
||||||
|
request: MemoryQueryRequest
|
||||||
|
correlation_id: str
|
||||||
|
total: int
|
||||||
|
items: tuple[MemoryRetrievalItem, ...] = ()
|
||||||
|
diagnostics: tuple[Diagnostic, ...] = ()
|
||||||
|
audit_event: AuditEvent | None = None
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
success: bool = True
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "items", tuple(self.items))
|
||||||
|
object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result_count(self) -> int:
|
||||||
|
return len(self.items)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_offset(self) -> int | None:
|
||||||
|
next_offset = self.request.offset + self.result_count
|
||||||
|
return next_offset if next_offset < self.total else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context_items(self) -> tuple[dict[str, Any], ...]:
|
||||||
|
return tuple(item.to_context_item() for item in self.items)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"query": self.request.to_dict(),
|
||||||
|
"correlation_id": self.correlation_id,
|
||||||
|
"success": self.success,
|
||||||
|
"total": self.total,
|
||||||
|
"result_count": self.result_count,
|
||||||
|
"next_offset": self.next_offset,
|
||||||
|
"metadata": dict(self.metadata),
|
||||||
|
"results": [item.to_dict() for item in self.items],
|
||||||
|
"context_items": list(self.context_items),
|
||||||
|
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||||
|
"audit_event": self.audit_event.to_dict() if self.audit_event else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MemoryRuntimeService:
|
class MemoryRuntimeService:
|
||||||
def __init__(self, repository: MemoryGraphRepository) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
repository: MemoryGraphRepository,
|
||||||
|
*,
|
||||||
|
policy_gateway: PolicyGateway | None = None,
|
||||||
|
) -> None:
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
|
self.policy_gateway = policy_gateway or AllowAllPolicyGateway()
|
||||||
|
|
||||||
def import_markitect_graph(
|
def import_markitect_graph(
|
||||||
self,
|
self,
|
||||||
@@ -98,3 +210,190 @@ class MemoryRuntimeService:
|
|||||||
"intent": imported.intent,
|
"intent": imported.intent,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def query_memory(
|
||||||
|
self,
|
||||||
|
request: MemoryQueryRequest,
|
||||||
|
context: OperationContext,
|
||||||
|
) -> MemoryRetrievalResult:
|
||||||
|
diagnostics = _validate_query(request)
|
||||||
|
if diagnostics:
|
||||||
|
return MemoryRetrievalResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
total=0,
|
||||||
|
diagnostics=tuple(diagnostics),
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_resource = f"memory-graph:{request.graph_id or '*'}"
|
||||||
|
scope_decision = self._authorize(
|
||||||
|
context,
|
||||||
|
"memory.query",
|
||||||
|
scope_resource,
|
||||||
|
resource_metadata={"query": request.to_dict()},
|
||||||
|
)
|
||||||
|
if not scope_decision.allowed:
|
||||||
|
audit_event = AuditEvent.from_context(
|
||||||
|
"memory.query",
|
||||||
|
scope_resource,
|
||||||
|
AuditOutcome.DENIED,
|
||||||
|
context,
|
||||||
|
policy_decision=scope_decision,
|
||||||
|
details={"query": request.to_dict()},
|
||||||
|
)
|
||||||
|
return MemoryRetrievalResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
total=0,
|
||||||
|
diagnostics=(_permission_denied_diagnostic(scope_decision),),
|
||||||
|
audit_event=audit_event,
|
||||||
|
success=False,
|
||||||
|
metadata={"policy_enforced": True, "permission_filtered_count": 0},
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes = self.repository.list_memory_nodes(graph_id=request.graph_id)
|
||||||
|
if request.node_ids:
|
||||||
|
wanted = set(request.node_ids)
|
||||||
|
nodes = [node for node in nodes if node.node_id in wanted or node.contract_node_id in wanted]
|
||||||
|
if request.kinds:
|
||||||
|
wanted_kinds = set(request.kinds)
|
||||||
|
nodes = [node for node in nodes if node.kind in wanted_kinds]
|
||||||
|
if request.text_contains:
|
||||||
|
needle = request.text_contains.casefold()
|
||||||
|
nodes = [node for node in nodes if needle in node.text.casefold()]
|
||||||
|
|
||||||
|
denied_count = 0
|
||||||
|
item_diagnostics: list[Diagnostic] = []
|
||||||
|
allowed_nodes: list[tuple[MemoryNodeRecord, PolicyDecision]] = []
|
||||||
|
for node in nodes:
|
||||||
|
decision = self._authorize(
|
||||||
|
context,
|
||||||
|
"memory.node.retrieve",
|
||||||
|
node.resource_id,
|
||||||
|
resource_metadata=_node_policy_metadata(node),
|
||||||
|
)
|
||||||
|
if decision.allowed:
|
||||||
|
allowed_nodes.append((node, decision))
|
||||||
|
else:
|
||||||
|
denied_count += 1
|
||||||
|
item_diagnostics.append(_permission_denied_diagnostic(decision, node=node))
|
||||||
|
|
||||||
|
allowed_node_ids = {str(node.node_id) for node, _ in allowed_nodes}
|
||||||
|
items: list[MemoryRetrievalItem] = []
|
||||||
|
for node, decision in allowed_nodes:
|
||||||
|
edges: tuple[MemoryEdgeRecord, ...] = ()
|
||||||
|
if request.include_edges:
|
||||||
|
edges = tuple(
|
||||||
|
edge
|
||||||
|
for edge in self.repository.list_memory_edges(graph_id=node.graph_id)
|
||||||
|
if (
|
||||||
|
edge.source_node_id == node.node_id
|
||||||
|
or edge.target_node_id == node.node_id
|
||||||
|
)
|
||||||
|
and edge.source_node_id in allowed_node_ids
|
||||||
|
and edge.target_node_id in allowed_node_ids
|
||||||
|
)
|
||||||
|
items.append(MemoryRetrievalItem(node=node, edges=edges, policy_decision=decision))
|
||||||
|
|
||||||
|
total = len(items)
|
||||||
|
page = tuple(items[request.offset : request.offset + request.limit])
|
||||||
|
outcome = AuditOutcome.PARTIAL if denied_count else AuditOutcome.SUCCESS
|
||||||
|
audit_event = AuditEvent.from_context(
|
||||||
|
"memory.query",
|
||||||
|
scope_resource,
|
||||||
|
outcome,
|
||||||
|
context,
|
||||||
|
policy_decision=scope_decision,
|
||||||
|
details={
|
||||||
|
"query": request.to_dict(),
|
||||||
|
"matched_count": len(nodes),
|
||||||
|
"permission_filtered_count": denied_count,
|
||||||
|
"result_count": len(page),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return MemoryRetrievalResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
total=total,
|
||||||
|
items=page,
|
||||||
|
diagnostics=tuple(item_diagnostics),
|
||||||
|
audit_event=audit_event,
|
||||||
|
metadata={
|
||||||
|
"zero_result": total == 0,
|
||||||
|
"policy_enforced": True,
|
||||||
|
"permission_filtered_count": denied_count,
|
||||||
|
"context_assembly": "memory-node-source-spans",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _authorize(
|
||||||
|
self,
|
||||||
|
context: OperationContext,
|
||||||
|
action: str,
|
||||||
|
resource: str,
|
||||||
|
*,
|
||||||
|
resource_metadata: dict[str, Any] | None = None,
|
||||||
|
) -> PolicyDecision:
|
||||||
|
try:
|
||||||
|
return self.policy_gateway.authorize(
|
||||||
|
context,
|
||||||
|
action,
|
||||||
|
resource,
|
||||||
|
resource_metadata=resource_metadata,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return PolicyDecision.fail_closed(
|
||||||
|
context.actor.id,
|
||||||
|
action,
|
||||||
|
resource,
|
||||||
|
reason=str(exc) or "Memory policy gateway failed",
|
||||||
|
context={
|
||||||
|
"gateway_error": type(exc).__name__,
|
||||||
|
"resource_metadata": resource_metadata or {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_query(request: MemoryQueryRequest) -> list[Diagnostic]:
|
||||||
|
diagnostics: list[Diagnostic] = []
|
||||||
|
if request.limit < 1:
|
||||||
|
diagnostics.append(Diagnostic("error", "memory.query.limit_invalid", "limit must be at least 1"))
|
||||||
|
if request.offset < 0:
|
||||||
|
diagnostics.append(Diagnostic("error", "memory.query.offset_invalid", "offset must not be negative"))
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _node_policy_metadata(node: MemoryNodeRecord) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"node_id": node.node_id,
|
||||||
|
"contract_node_id": node.contract_node_id,
|
||||||
|
"graph_id": node.graph_id,
|
||||||
|
"contract_graph_id": node.contract_graph_id,
|
||||||
|
"kind": node.kind,
|
||||||
|
"namespace": dict(node.namespace),
|
||||||
|
"policy": dict(node.policy),
|
||||||
|
"metadata": dict(node.metadata),
|
||||||
|
"source_spans": [span.to_dict() for span in node.source_spans],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _permission_denied_diagnostic(
|
||||||
|
decision: PolicyDecision,
|
||||||
|
*,
|
||||||
|
node: MemoryNodeRecord | None = None,
|
||||||
|
) -> Diagnostic:
|
||||||
|
details: dict[str, Any] = {
|
||||||
|
"policy_decision": decision.to_dict(),
|
||||||
|
"resource": decision.resource,
|
||||||
|
}
|
||||||
|
if node:
|
||||||
|
details["node_id"] = node.node_id
|
||||||
|
details["contract_node_id"] = node.contract_node_id
|
||||||
|
details["kind"] = node.kind
|
||||||
|
return Diagnostic(
|
||||||
|
"warning",
|
||||||
|
"memory.permission_denied",
|
||||||
|
decision.reason or "Memory retrieval denied by policy.",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ from kontextual_engine import (
|
|||||||
DuplicateResourceError,
|
DuplicateResourceError,
|
||||||
InMemoryMemoryGraphRepository,
|
InMemoryMemoryGraphRepository,
|
||||||
MemoryGraphImportResult,
|
MemoryGraphImportResult,
|
||||||
|
MemoryQueryRequest,
|
||||||
MemoryRuntimeService,
|
MemoryRuntimeService,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
|
PolicyDecision,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,6 +73,67 @@ def test_memory_runtime_service_imports_contracts_and_reports_audit_context() ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_query_returns_source_grounded_context_items_and_edges() -> None:
|
||||||
|
repo = InMemoryMemoryGraphRepository()
|
||||||
|
service = MemoryRuntimeService(repo)
|
||||||
|
summary = service.import_markitect_graph(_graph_contract())
|
||||||
|
|
||||||
|
result = service.query_memory(
|
||||||
|
MemoryQueryRequest(graph_id=summary.graph_id),
|
||||||
|
operation_context(),
|
||||||
|
)
|
||||||
|
decision = next(item for item in result.items if item.node.contract_node_id == "decision.contract-boundary")
|
||||||
|
|
||||||
|
assert result.success is True
|
||||||
|
assert result.total == 2
|
||||||
|
assert decision.to_context_item()["source_spans"][0]["path"] == "workplans/MKTT-WP-0016.md"
|
||||||
|
assert decision.to_context_item()["metadata"]["edges"][0]["contract_edge_id"] == "edge.boundary-support"
|
||||||
|
assert result.metadata["policy_enforced"] is True
|
||||||
|
assert result.audit_event is not None
|
||||||
|
assert result.audit_event.operation == "memory.query"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_query_filters_denied_nodes_without_leaking_denied_text() -> None:
|
||||||
|
repo = InMemoryMemoryGraphRepository()
|
||||||
|
service = MemoryRuntimeService(repo, policy_gateway=DenyInternalMemoryPolicy())
|
||||||
|
graph = _graph_contract()
|
||||||
|
graph["nodes"].append(
|
||||||
|
{
|
||||||
|
"id": "claim.internal-secret",
|
||||||
|
"kind": "claim",
|
||||||
|
"text": "secret memory text must not leak",
|
||||||
|
"policy": {"labels": ["internal"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
service.import_markitect_graph(graph)
|
||||||
|
|
||||||
|
result = service.query_memory(MemoryQueryRequest(text_contains="memory"), operation_context())
|
||||||
|
|
||||||
|
assert result.success is True
|
||||||
|
assert result.metadata["permission_filtered_count"] == 1
|
||||||
|
assert all(item.node.contract_node_id != "claim.internal-secret" for item in result.items)
|
||||||
|
assert "secret memory text must not leak" not in str(result.to_dict())
|
||||||
|
assert result.diagnostics[0].code == "memory.permission_denied"
|
||||||
|
assert result.diagnostics[0].details["contract_node_id"] == "claim.internal-secret"
|
||||||
|
assert result.audit_event is not None
|
||||||
|
assert result.audit_event.outcome.value == "partial"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_query_scope_policy_fail_closed_returns_empty_result() -> None:
|
||||||
|
repo = InMemoryMemoryGraphRepository()
|
||||||
|
service = MemoryRuntimeService(repo, policy_gateway=BrokenMemoryPolicy())
|
||||||
|
service.import_markitect_graph(_graph_contract())
|
||||||
|
|
||||||
|
result = service.query_memory(MemoryQueryRequest(), operation_context())
|
||||||
|
|
||||||
|
assert result.success is False
|
||||||
|
assert result.items == ()
|
||||||
|
assert result.total == 0
|
||||||
|
assert result.diagnostics[0].details["policy_decision"]["effect"] == "fail_closed"
|
||||||
|
assert result.audit_event is not None
|
||||||
|
assert result.audit_event.outcome.value == "denied"
|
||||||
|
|
||||||
|
|
||||||
def test_memory_runtime_service_rejects_invalid_edge_contracts() -> None:
|
def test_memory_runtime_service_rejects_invalid_edge_contracts() -> None:
|
||||||
repo = InMemoryMemoryGraphRepository()
|
repo = InMemoryMemoryGraphRepository()
|
||||||
service = MemoryRuntimeService(repo)
|
service = MemoryRuntimeService(repo)
|
||||||
@@ -83,6 +146,52 @@ def test_memory_runtime_service_rejects_invalid_edge_contracts() -> None:
|
|||||||
assert "unknown node" in str(exc.value)
|
assert "unknown node" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def operation_context() -> OperationContext:
|
||||||
|
actor = Actor.create(ActorType.HUMAN, actor_id="user-memory", display_name="Memory Tester")
|
||||||
|
return OperationContext.create(actor, correlation_id="corr-memory")
|
||||||
|
|
||||||
|
|
||||||
|
class DenyInternalMemoryPolicy:
|
||||||
|
def authorize(
|
||||||
|
self,
|
||||||
|
context: OperationContext,
|
||||||
|
action: str,
|
||||||
|
resource: str,
|
||||||
|
*,
|
||||||
|
resource_metadata: dict | None = None,
|
||||||
|
) -> PolicyDecision:
|
||||||
|
resource_metadata = resource_metadata or {}
|
||||||
|
labels = resource_metadata.get("policy", {}).get("labels", ())
|
||||||
|
if action == "memory.node.retrieve" and "internal" in labels:
|
||||||
|
return PolicyDecision.fail_closed(
|
||||||
|
context.actor.id,
|
||||||
|
action,
|
||||||
|
resource,
|
||||||
|
reason="internal memory denied in test policy",
|
||||||
|
context={"resource_metadata": resource_metadata},
|
||||||
|
)
|
||||||
|
return PolicyDecision.allow(
|
||||||
|
context.actor.id,
|
||||||
|
action,
|
||||||
|
resource,
|
||||||
|
context={"resource_metadata": resource_metadata},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BrokenMemoryPolicy:
|
||||||
|
def authorize(
|
||||||
|
self,
|
||||||
|
context: OperationContext,
|
||||||
|
action: str,
|
||||||
|
resource: str,
|
||||||
|
*,
|
||||||
|
resource_metadata: dict | None = None,
|
||||||
|
) -> PolicyDecision:
|
||||||
|
if action == "memory.query":
|
||||||
|
raise RuntimeError("memory policy context unavailable")
|
||||||
|
return PolicyDecision.allow(context.actor.id, action, resource)
|
||||||
|
|
||||||
|
|
||||||
def _profile_contract() -> dict:
|
def _profile_contract() -> dict:
|
||||||
return {
|
return {
|
||||||
"schema_version": "markitect.memory.profile.v1",
|
"schema_version": "markitect.memory.profile.v1",
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ Primary files:
|
|||||||
- `src/kontextual_engine/services/memory_service.py`
|
- `src/kontextual_engine/services/memory_service.py`
|
||||||
- `docs/memory-graph-runtime.md`
|
- `docs/memory-graph-runtime.md`
|
||||||
|
|
||||||
|
The permission-aware retrieval slice is also implemented in
|
||||||
|
`MemoryRuntimeService.query_memory()`. It performs a scope policy check,
|
||||||
|
per-node policy checks, returns source-grounded context items, preserves denied
|
||||||
|
diagnostics without leaking denied node text, and emits an audit event in the
|
||||||
|
result envelope.
|
||||||
|
|
||||||
## P17.1 - Import and map Markitect memory contracts
|
## P17.1 - Import and map Markitect memory contracts
|
||||||
|
|
||||||
```task
|
```task
|
||||||
@@ -130,7 +136,7 @@ tests.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0017-T003
|
id: KONT-WP-0017-T003
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "711b945e-19e6-4548-8ddb-6c2c364148ca"
|
state_hub_task_id: "711b945e-19e6-4548-8ddb-6c2c364148ca"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user