generated from coulomb/repo-seed
feat(memory): add audit export surface
This commit is contained in:
@@ -21,10 +21,10 @@ with `markitect-tool`.
|
|||||||
- `InMemoryMemoryGraphRepository` provides deterministic local storage for
|
- `InMemoryMemoryGraphRepository` provides deterministic local storage for
|
||||||
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.
|
persists an audit event when an `OperationContext` is supplied.
|
||||||
- `MemoryRuntimeService.query_memory()` retrieves graph nodes through a scope
|
- `MemoryRuntimeService.query_memory()` retrieves graph nodes through a scope
|
||||||
policy check plus per-node policy checks, returns source-grounded context
|
policy check plus per-node policy checks, returns source-grounded context
|
||||||
items, preserves safe denied diagnostics, and emits an audit event in the
|
items, preserves safe denied diagnostics, and persists an audit event in the
|
||||||
result envelope.
|
result envelope.
|
||||||
- `MemoryRuntimeService.apply_retention()` marks stale memories for review or
|
- `MemoryRuntimeService.apply_retention()` marks stale memories for review or
|
||||||
transitions old memories to `delete_requested` without physical deletion.
|
transitions old memories to `delete_requested` without physical deletion.
|
||||||
@@ -40,6 +40,12 @@ with `markitect-tool`.
|
|||||||
- `MemoryRuntimeService.export_context_package_inputs()` emits
|
- `MemoryRuntimeService.export_context_package_inputs()` emits
|
||||||
Markitect-compatible context package input envelopes without invoking the
|
Markitect-compatible context package input envelopes without invoking the
|
||||||
Markitect compiler.
|
Markitect compiler.
|
||||||
|
- `MemoryGraphRepository` persists memory audit events separately from
|
||||||
|
Markitect memory events, allowing operations to be queried by graph,
|
||||||
|
correlation id, and operation.
|
||||||
|
- `MemoryRuntimeService.export_runtime_envelope()` emits a portable runtime
|
||||||
|
envelope containing graph nodes, edges, memory events, and audit traces with
|
||||||
|
operation id, actor, policy decision, and filter metadata.
|
||||||
|
|
||||||
## Boundary
|
## Boundary
|
||||||
|
|
||||||
@@ -53,6 +59,7 @@ with `markitect-tool`.
|
|||||||
|
|
||||||
- runtime ids and persistence
|
- runtime ids and persistence
|
||||||
- append-only event storage
|
- append-only event storage
|
||||||
|
- durable audit event storage
|
||||||
- permission-aware retrieval and context assembly
|
- permission-aware retrieval and context assembly
|
||||||
- retention, refresh, compaction, review gates, and audit behavior
|
- retention, refresh, compaction, review gates, and audit behavior
|
||||||
- agent-safe update plans and Markitect-compatible export envelopes
|
- agent-safe update plans and Markitect-compatible export envelopes
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ from .services import (
|
|||||||
MemoryRetrievalItem,
|
MemoryRetrievalItem,
|
||||||
MemoryRetrievalResult,
|
MemoryRetrievalResult,
|
||||||
MemoryRetentionRequest,
|
MemoryRetentionRequest,
|
||||||
|
MemoryRuntimeExportRequest,
|
||||||
|
MemoryRuntimeExportResult,
|
||||||
MemoryRuntimeService,
|
MemoryRuntimeService,
|
||||||
MemoryUpdatePlan,
|
MemoryUpdatePlan,
|
||||||
MemoryUpdateRequest,
|
MemoryUpdateRequest,
|
||||||
@@ -288,6 +290,8 @@ __all__ = [
|
|||||||
"MemoryRetrievalItem",
|
"MemoryRetrievalItem",
|
||||||
"MemoryRetrievalResult",
|
"MemoryRetrievalResult",
|
||||||
"MemoryRetentionRequest",
|
"MemoryRetentionRequest",
|
||||||
|
"MemoryRuntimeExportRequest",
|
||||||
|
"MemoryRuntimeExportResult",
|
||||||
"MemoryRuntimeService",
|
"MemoryRuntimeService",
|
||||||
"MemorySourceSpan",
|
"MemorySourceSpan",
|
||||||
"MemoryUpdatePlan",
|
"MemoryUpdatePlan",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ from .memory_service import (
|
|||||||
MemoryRetrievalItem,
|
MemoryRetrievalItem,
|
||||||
MemoryRetrievalResult,
|
MemoryRetrievalResult,
|
||||||
MemoryRetentionRequest,
|
MemoryRetentionRequest,
|
||||||
|
MemoryRuntimeExportRequest,
|
||||||
|
MemoryRuntimeExportResult,
|
||||||
MemoryRuntimeService,
|
MemoryRuntimeService,
|
||||||
MemoryUpdatePlan,
|
MemoryUpdatePlan,
|
||||||
MemoryUpdateRequest,
|
MemoryUpdateRequest,
|
||||||
@@ -85,6 +87,8 @@ __all__ = [
|
|||||||
"MemoryRetrievalItem",
|
"MemoryRetrievalItem",
|
||||||
"MemoryRetrievalResult",
|
"MemoryRetrievalResult",
|
||||||
"MemoryRetentionRequest",
|
"MemoryRetentionRequest",
|
||||||
|
"MemoryRuntimeExportRequest",
|
||||||
|
"MemoryRuntimeExportResult",
|
||||||
"MemoryRuntimeService",
|
"MemoryRuntimeService",
|
||||||
"MemoryUpdatePlan",
|
"MemoryUpdatePlan",
|
||||||
"MemoryUpdateRequest",
|
"MemoryUpdateRequest",
|
||||||
|
|||||||
@@ -537,18 +537,20 @@ class MemoryRuntimeService:
|
|||||||
|
|
||||||
audit_event = None
|
audit_event = None
|
||||||
if context:
|
if context:
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.import_markitect_graph",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{imported.graph_id}",
|
"memory.import_markitect_graph",
|
||||||
AuditOutcome.SUCCESS,
|
f"memory-graph:{imported.graph_id}",
|
||||||
context,
|
AuditOutcome.SUCCESS,
|
||||||
details={
|
context,
|
||||||
"contract_graph_id": imported.contract_graph_id,
|
details={
|
||||||
"profile_id": imported.profile.profile_id if imported.profile else None,
|
"contract_graph_id": imported.contract_graph_id,
|
||||||
"nodes": len(imported.nodes),
|
"profile_id": imported.profile.profile_id if imported.profile else None,
|
||||||
"edges": len(imported.edges),
|
"nodes": len(imported.nodes),
|
||||||
"events": len(imported.events),
|
"edges": len(imported.edges),
|
||||||
},
|
"events": len(imported.events),
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryGraphImportSummary(
|
return MemoryGraphImportSummary(
|
||||||
graph_id=imported.graph_id,
|
graph_id=imported.graph_id,
|
||||||
@@ -588,13 +590,15 @@ class MemoryRuntimeService:
|
|||||||
resource_metadata={"query": request.to_dict()},
|
resource_metadata={"query": request.to_dict()},
|
||||||
)
|
)
|
||||||
if not scope_decision.allowed:
|
if not scope_decision.allowed:
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.query",
|
AuditEvent.from_context(
|
||||||
scope_resource,
|
"memory.query",
|
||||||
AuditOutcome.DENIED,
|
scope_resource,
|
||||||
context,
|
AuditOutcome.DENIED,
|
||||||
policy_decision=scope_decision,
|
context,
|
||||||
details={"query": request.to_dict()},
|
policy_decision=scope_decision,
|
||||||
|
details={"query": request.to_dict()},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryRetrievalResult(
|
return MemoryRetrievalResult(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -653,18 +657,20 @@ class MemoryRuntimeService:
|
|||||||
total = len(items)
|
total = len(items)
|
||||||
page = tuple(items[request.offset : request.offset + request.limit])
|
page = tuple(items[request.offset : request.offset + request.limit])
|
||||||
outcome = AuditOutcome.PARTIAL if denied_count else AuditOutcome.SUCCESS
|
outcome = AuditOutcome.PARTIAL if denied_count else AuditOutcome.SUCCESS
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.query",
|
AuditEvent.from_context(
|
||||||
scope_resource,
|
"memory.query",
|
||||||
outcome,
|
scope_resource,
|
||||||
context,
|
outcome,
|
||||||
policy_decision=scope_decision,
|
context,
|
||||||
details={
|
policy_decision=scope_decision,
|
||||||
"query": request.to_dict(),
|
details={
|
||||||
"matched_count": len(nodes),
|
"query": request.to_dict(),
|
||||||
"permission_filtered_count": denied_count,
|
"matched_count": len(nodes),
|
||||||
"result_count": len(page),
|
"permission_filtered_count": denied_count,
|
||||||
},
|
"result_count": len(page),
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryRetrievalResult(
|
return MemoryRetrievalResult(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -737,16 +743,18 @@ class MemoryRuntimeService:
|
|||||||
node_updates=[update.to_dict() for update in updates],
|
node_updates=[update.to_dict() for update in updates],
|
||||||
metadata={"request": request.to_dict()},
|
metadata={"request": request.to_dict()},
|
||||||
)
|
)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.retention.apply",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{request.graph_id}",
|
"memory.retention.apply",
|
||||||
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
f"memory-graph:{request.graph_id}",
|
||||||
context,
|
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
||||||
details={
|
context,
|
||||||
"request": request.to_dict(),
|
details={
|
||||||
"updated_nodes": len(updates),
|
"request": request.to_dict(),
|
||||||
"event_id": event.event_id if event else None,
|
"updated_nodes": len(updates),
|
||||||
},
|
"event_id": event.event_id if event else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryLifecycleResult(
|
return MemoryLifecycleResult(
|
||||||
operation="memory.retention.apply",
|
operation="memory.retention.apply",
|
||||||
@@ -797,16 +805,18 @@ class MemoryRuntimeService:
|
|||||||
node_updates=[update.to_dict() for update in updates],
|
node_updates=[update.to_dict() for update in updates],
|
||||||
metadata={"request": request.to_dict()},
|
metadata={"request": request.to_dict()},
|
||||||
)
|
)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.refresh",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{request.graph_id}",
|
"memory.refresh",
|
||||||
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
f"memory-graph:{request.graph_id}",
|
||||||
context,
|
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
||||||
details={
|
context,
|
||||||
"request": request.to_dict(),
|
details={
|
||||||
"updated_nodes": len(updates),
|
"request": request.to_dict(),
|
||||||
"event_id": event.event_id if event else None,
|
"updated_nodes": len(updates),
|
||||||
},
|
"event_id": event.event_id if event else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryLifecycleResult(
|
return MemoryLifecycleResult(
|
||||||
operation="memory.refresh",
|
operation="memory.refresh",
|
||||||
@@ -891,17 +901,19 @@ class MemoryRuntimeService:
|
|||||||
"operation_id": operation_id,
|
"operation_id": operation_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.compact",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{request.graph_id}",
|
"memory.compact",
|
||||||
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
f"memory-graph:{request.graph_id}",
|
||||||
context,
|
AuditOutcome.DRY_RUN if request.dry_run else AuditOutcome.SUCCESS,
|
||||||
details={
|
context,
|
||||||
"request": request.to_dict(),
|
details={
|
||||||
"summary_node_id": summary.node_id,
|
"request": request.to_dict(),
|
||||||
"source_nodes": len(nodes),
|
"summary_node_id": summary.node_id,
|
||||||
"event_id": event.event_id if event else None,
|
"source_nodes": len(nodes),
|
||||||
},
|
"event_id": event.event_id if event else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryLifecycleResult(
|
return MemoryLifecycleResult(
|
||||||
operation="memory.compact",
|
operation="memory.compact",
|
||||||
@@ -979,12 +991,14 @@ class MemoryRuntimeService:
|
|||||||
"Memory update requires explicit review approval before durable write.",
|
"Memory update requires explicit review approval before durable write.",
|
||||||
details={"plan_id": plan.plan_id, "review_decision": review_decision},
|
details={"plan_id": plan.plan_id, "review_decision": review_decision},
|
||||||
)
|
)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.update.apply",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{plan.request.graph_id}",
|
"memory.update.apply",
|
||||||
AuditOutcome.REVIEW_REQUIRED,
|
f"memory-graph:{plan.request.graph_id}",
|
||||||
context,
|
AuditOutcome.REVIEW_REQUIRED,
|
||||||
details={"plan_id": plan.plan_id, "review_decision": review_decision},
|
context,
|
||||||
|
details={"plan_id": plan.plan_id, "review_decision": review_decision},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryUpdateResult(
|
return MemoryUpdateResult(
|
||||||
plan=plan,
|
plan=plan,
|
||||||
@@ -1001,13 +1015,15 @@ class MemoryRuntimeService:
|
|||||||
)
|
)
|
||||||
if not decision.allowed:
|
if not decision.allowed:
|
||||||
diagnostic = _permission_denied_diagnostic(decision)
|
diagnostic = _permission_denied_diagnostic(decision)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.update.apply",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{plan.request.graph_id}",
|
"memory.update.apply",
|
||||||
AuditOutcome.DENIED,
|
f"memory-graph:{plan.request.graph_id}",
|
||||||
context,
|
AuditOutcome.DENIED,
|
||||||
policy_decision=decision,
|
context,
|
||||||
details={"plan_id": plan.plan_id},
|
policy_decision=decision,
|
||||||
|
details={"plan_id": plan.plan_id},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryUpdateResult(plan=plan, audit_event=audit_event, diagnostics=(diagnostic,), success=False)
|
return MemoryUpdateResult(plan=plan, audit_event=audit_event, diagnostics=(diagnostic,), success=False)
|
||||||
for update in plan.planned_updates:
|
for update in plan.planned_updates:
|
||||||
@@ -1019,13 +1035,15 @@ class MemoryRuntimeService:
|
|||||||
node_updates=[_planned_update_event_payload(update) for update in plan.planned_updates],
|
node_updates=[_planned_update_event_payload(update) for update in plan.planned_updates],
|
||||||
metadata={"plan_id": plan.plan_id, "review_decision": review_decision},
|
metadata={"plan_id": plan.plan_id, "review_decision": review_decision},
|
||||||
)
|
)
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.update.apply",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{plan.request.graph_id}",
|
"memory.update.apply",
|
||||||
AuditOutcome.SUCCESS,
|
f"memory-graph:{plan.request.graph_id}",
|
||||||
context,
|
AuditOutcome.SUCCESS,
|
||||||
policy_decision=decision,
|
context,
|
||||||
details={"plan_id": plan.plan_id, "applied_nodes": len(plan.planned_updates), "event_id": event.event_id},
|
policy_decision=decision,
|
||||||
|
details={"plan_id": plan.plan_id, "applied_nodes": len(plan.planned_updates), "event_id": event.event_id},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryUpdateResult(
|
return MemoryUpdateResult(
|
||||||
plan=plan,
|
plan=plan,
|
||||||
@@ -1050,16 +1068,18 @@ class MemoryRuntimeService:
|
|||||||
)
|
)
|
||||||
package_input = _markitect_package_input(request, retrieval)
|
package_input = _markitect_package_input(request, retrieval)
|
||||||
outcome = AuditOutcome.PARTIAL if retrieval.metadata.get("permission_filtered_count") else AuditOutcome.SUCCESS
|
outcome = AuditOutcome.PARTIAL if retrieval.metadata.get("permission_filtered_count") else AuditOutcome.SUCCESS
|
||||||
audit_event = AuditEvent.from_context(
|
audit_event = self._record_audit(
|
||||||
"memory.package.export",
|
AuditEvent.from_context(
|
||||||
f"memory-graph:{request.query.graph_id or '*'}",
|
"memory.package.export",
|
||||||
outcome,
|
f"memory-graph:{request.query.graph_id or '*'}",
|
||||||
context,
|
outcome,
|
||||||
details={
|
context,
|
||||||
"request": request.to_dict(),
|
details={
|
||||||
"items": len(package_input.get("items", ())),
|
"request": request.to_dict(),
|
||||||
"permission_filtered_count": retrieval.metadata.get("permission_filtered_count", 0),
|
"items": len(package_input.get("items", ())),
|
||||||
},
|
"permission_filtered_count": retrieval.metadata.get("permission_filtered_count", 0),
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return MemoryPackageExportResult(
|
return MemoryPackageExportResult(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -1070,6 +1090,132 @@ class MemoryRuntimeService:
|
|||||||
diagnostics=retrieval.diagnostics,
|
diagnostics=retrieval.diagnostics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def export_runtime_envelope(
|
||||||
|
self,
|
||||||
|
request: MemoryRuntimeExportRequest,
|
||||||
|
context: OperationContext,
|
||||||
|
) -> MemoryRuntimeExportResult:
|
||||||
|
diagnostics = _validate_runtime_export_request(request)
|
||||||
|
if diagnostics:
|
||||||
|
return MemoryRuntimeExportResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
diagnostics=tuple(diagnostics),
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
target = f"memory-graph:{request.graph_id}"
|
||||||
|
decision = self._authorize(
|
||||||
|
context,
|
||||||
|
"memory.runtime.export",
|
||||||
|
target,
|
||||||
|
resource_metadata={"request": request.to_dict()},
|
||||||
|
)
|
||||||
|
if not decision.allowed:
|
||||||
|
audit_event = self._record_audit(
|
||||||
|
AuditEvent.from_context(
|
||||||
|
"memory.runtime.export",
|
||||||
|
target,
|
||||||
|
AuditOutcome.DENIED,
|
||||||
|
context,
|
||||||
|
policy_decision=decision,
|
||||||
|
details={"request": request.to_dict()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return MemoryRuntimeExportResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
audit_event=audit_event,
|
||||||
|
diagnostics=(_permission_denied_diagnostic(decision),),
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
operation_id = new_id("memexport")
|
||||||
|
exported_at = utc_now().isoformat()
|
||||||
|
all_nodes = self.repository.list_memory_nodes(graph_id=request.graph_id)
|
||||||
|
nodes = _runtime_export_nodes(all_nodes, include_retired=request.include_retired)
|
||||||
|
visible_node_ids = {str(node.node_id) for node in nodes}
|
||||||
|
edges: list[MemoryEdgeRecord] = []
|
||||||
|
if request.include_edges:
|
||||||
|
edges = self.repository.list_memory_edges(graph_id=request.graph_id)
|
||||||
|
if not request.include_retired:
|
||||||
|
edges = [
|
||||||
|
edge
|
||||||
|
for edge in edges
|
||||||
|
if edge.lifecycle not in _runtime_export_hidden_lifecycles()
|
||||||
|
and edge.source_node_id in visible_node_ids
|
||||||
|
and edge.target_node_id in visible_node_ids
|
||||||
|
]
|
||||||
|
events: list[MemoryEventRecord] = []
|
||||||
|
if request.include_events:
|
||||||
|
wanted_kinds = set(request.event_kinds)
|
||||||
|
events = [
|
||||||
|
event
|
||||||
|
for event in self.repository.list_memory_events(graph_id=request.graph_id)
|
||||||
|
if not wanted_kinds or event.kind in wanted_kinds
|
||||||
|
]
|
||||||
|
audit_events: list[AuditEvent] = []
|
||||||
|
if request.include_audit_events:
|
||||||
|
wanted_operations = set(request.operations)
|
||||||
|
audit_events = [
|
||||||
|
event
|
||||||
|
for event in self.repository.list_memory_audit_events(
|
||||||
|
graph_id=request.graph_id,
|
||||||
|
correlation_id=request.correlation_id,
|
||||||
|
)
|
||||||
|
if not wanted_operations or event.operation in wanted_operations
|
||||||
|
]
|
||||||
|
export_nodes = nodes if request.include_nodes else []
|
||||||
|
counts = {
|
||||||
|
"nodes": len(export_nodes),
|
||||||
|
"edges": len(edges),
|
||||||
|
"events": len(events),
|
||||||
|
"audit_events": len(audit_events),
|
||||||
|
}
|
||||||
|
envelope = {
|
||||||
|
"schema_version": "kontextual.memory.runtime-export.v1",
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"graph_id": request.graph_id,
|
||||||
|
"exported_at": exported_at,
|
||||||
|
"actor": context.actor.to_dict(),
|
||||||
|
"correlation_id": context.correlation_id,
|
||||||
|
"request": request.to_dict(),
|
||||||
|
"nodes": [node.to_dict() for node in export_nodes],
|
||||||
|
"edges": [edge.to_dict() for edge in edges],
|
||||||
|
"events": [event.to_dict() for event in events],
|
||||||
|
"audit_events": [event.to_dict() for event in audit_events],
|
||||||
|
"metadata": {
|
||||||
|
"counts": counts,
|
||||||
|
"filters": {
|
||||||
|
"event_kinds": list(request.event_kinds),
|
||||||
|
"operations": list(request.operations),
|
||||||
|
"correlation_id": request.correlation_id,
|
||||||
|
"include_retired": request.include_retired,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
audit_event = self._record_audit(
|
||||||
|
AuditEvent.from_context(
|
||||||
|
"memory.runtime.export",
|
||||||
|
target,
|
||||||
|
AuditOutcome.SUCCESS,
|
||||||
|
context,
|
||||||
|
policy_decision=decision,
|
||||||
|
details={
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"graph_id": request.graph_id,
|
||||||
|
"request": request.to_dict(),
|
||||||
|
"counts": counts,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return MemoryRuntimeExportResult(
|
||||||
|
request=request,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
envelope=envelope,
|
||||||
|
audit_event=audit_event,
|
||||||
|
)
|
||||||
|
|
||||||
def _authorize(
|
def _authorize(
|
||||||
self,
|
self,
|
||||||
context: OperationContext,
|
context: OperationContext,
|
||||||
@@ -1097,6 +1243,9 @@ class MemoryRuntimeService:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _record_audit(self, audit_event: AuditEvent) -> AuditEvent:
|
||||||
|
return self.repository.save_memory_audit_event(audit_event)
|
||||||
|
|
||||||
def _append_lifecycle_event(
|
def _append_lifecycle_event(
|
||||||
self,
|
self,
|
||||||
event_kind: str,
|
event_kind: str,
|
||||||
@@ -1133,6 +1282,25 @@ def _validate_query(request: MemoryQueryRequest) -> list[Diagnostic]:
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_runtime_export_request(request: MemoryRuntimeExportRequest) -> list[Diagnostic]:
|
||||||
|
return _validate_node_selection_request(request.graph_id, ())
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_export_nodes(
|
||||||
|
nodes: list[MemoryNodeRecord],
|
||||||
|
*,
|
||||||
|
include_retired: bool,
|
||||||
|
) -> list[MemoryNodeRecord]:
|
||||||
|
if include_retired:
|
||||||
|
return nodes
|
||||||
|
hidden_lifecycles = _runtime_export_hidden_lifecycles()
|
||||||
|
return [node for node in nodes if node.lifecycle not in hidden_lifecycles]
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_export_hidden_lifecycles() -> set[LifecycleState]:
|
||||||
|
return {LifecycleState.RETIRED, LifecycleState.DELETE_REQUESTED, LifecycleState.DELETED}
|
||||||
|
|
||||||
|
|
||||||
def _node_policy_metadata(node: MemoryNodeRecord) -> dict[str, Any]:
|
def _node_policy_metadata(node: MemoryNodeRecord) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"node_id": node.node_id,
|
"node_id": node.node_id,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from kontextual_engine import (
|
|||||||
MemoryRefreshRequest,
|
MemoryRefreshRequest,
|
||||||
MemoryRetentionRequest,
|
MemoryRetentionRequest,
|
||||||
MemoryQueryRequest,
|
MemoryQueryRequest,
|
||||||
|
MemoryRuntimeExportRequest,
|
||||||
MemoryRuntimeService,
|
MemoryRuntimeService,
|
||||||
MemoryUpdateRequest,
|
MemoryUpdateRequest,
|
||||||
OperationContext,
|
OperationContext,
|
||||||
@@ -73,6 +74,7 @@ def test_memory_runtime_service_imports_contracts_and_reports_audit_context() ->
|
|||||||
assert summary.audit_event is not None
|
assert summary.audit_event is not None
|
||||||
assert summary.audit_event.actor_id == "agent-codex"
|
assert summary.audit_event.actor_id == "agent-codex"
|
||||||
assert summary.audit_event.correlation_id == "corr-memory"
|
assert summary.audit_event.correlation_id == "corr-memory"
|
||||||
|
assert repo.get_memory_audit_event(summary.audit_event.event_id) == summary.audit_event
|
||||||
assert repo.get_memory_profile(summary.profile_id).memory_kinds == (
|
assert repo.get_memory_profile(summary.profile_id).memory_kinds == (
|
||||||
"reasoning",
|
"reasoning",
|
||||||
"knowledge",
|
"knowledge",
|
||||||
@@ -283,6 +285,53 @@ def test_memory_package_export_emits_markitect_context_package_inputs_without_de
|
|||||||
assert export.audit_event.operation == "memory.package.export"
|
assert export.audit_event.operation == "memory.package.export"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_runtime_export_includes_persisted_audit_and_event_traces() -> None:
|
||||||
|
repo = InMemoryMemoryGraphRepository()
|
||||||
|
service = MemoryRuntimeService(repo)
|
||||||
|
context = operation_context()
|
||||||
|
summary = service.import_markitect_graph(_graph_contract(), context=context)
|
||||||
|
|
||||||
|
query = service.query_memory(MemoryQueryRequest(graph_id=summary.graph_id), context)
|
||||||
|
retention = service.apply_retention(
|
||||||
|
MemoryRetentionRequest(graph_id=summary.graph_id, stale_after_days=0),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
audit_operations = {
|
||||||
|
event.operation
|
||||||
|
for event in repo.list_memory_audit_events(
|
||||||
|
graph_id=summary.graph_id,
|
||||||
|
correlation_id=context.correlation_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export = service.export_runtime_envelope(
|
||||||
|
MemoryRuntimeExportRequest(
|
||||||
|
graph_id=summary.graph_id,
|
||||||
|
event_kinds=("recorded", "retention"),
|
||||||
|
operations=("memory.query", "memory.retention.apply"),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert query.success is True
|
||||||
|
assert retention.appended_events[0].kind == "retention"
|
||||||
|
assert {"memory.import_markitect_graph", "memory.query", "memory.retention.apply"} <= audit_operations
|
||||||
|
assert export.success is True
|
||||||
|
assert export.envelope["schema_version"] == "kontextual.memory.runtime-export.v1"
|
||||||
|
assert export.envelope["metadata"]["counts"] == {
|
||||||
|
"nodes": 2,
|
||||||
|
"edges": 1,
|
||||||
|
"events": 2,
|
||||||
|
"audit_events": 2,
|
||||||
|
}
|
||||||
|
assert {event["kind"] for event in export.envelope["events"]} == {"recorded", "retention"}
|
||||||
|
assert {event["operation"] for event in export.envelope["audit_events"]} == {
|
||||||
|
"memory.query",
|
||||||
|
"memory.retention.apply",
|
||||||
|
}
|
||||||
|
assert export.audit_event is not None
|
||||||
|
assert repo.get_memory_audit_event(export.audit_event.event_id) == export.audit_event
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ review gates for durable writes, source/policy explanations on planned node
|
|||||||
updates, and Markitect-compatible context package input export. The export
|
updates, and Markitect-compatible context package input export. The export
|
||||||
method emits package inputs only; Markitect remains the package compiler.
|
method emits package inputs only; Markitect remains the package compiler.
|
||||||
|
|
||||||
|
The observability/export slice is implemented. Memory audit events are now
|
||||||
|
persisted through the memory graph repository, queryable by graph, correlation,
|
||||||
|
and operation. Runtime export envelopes include graph nodes, edges, memory
|
||||||
|
events, audit traces, operation id, actor metadata, policy decisions, and
|
||||||
|
filter metadata for portable inspection or handoff.
|
||||||
|
|
||||||
## P17.1 - Import and map Markitect memory contracts
|
## P17.1 - Import and map Markitect memory contracts
|
||||||
|
|
||||||
```task
|
```task
|
||||||
@@ -215,7 +221,7 @@ Output: API contracts, tests, and agent-safe operation notes.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0017-T006
|
id: KONT-WP-0017-T006
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "54f51f87-6420-4170-913f-f6dae098fe71"
|
state_hub_task_id: "54f51f87-6420-4170-913f-f6dae098fe71"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user