Implement credentialed drill packaging workplan

This commit is contained in:
2026-05-19 01:27:59 +02:00
parent 022cd8d37e
commit 6e0372d21a
23 changed files with 924 additions and 43 deletions

View File

@@ -10,7 +10,15 @@ from .bridge import (
package_response_envelope,
)
from .contracts import graph_from_markitect, profile_from_markitect
from .evaluation import EVALUATION_REPORT_SCHEMA, evaluation_threshold_report
from .credentialed_drills import (
CREDENTIALED_ADAPTER_ENV_VARS,
CREDENTIALED_DRILL_SCHEMA,
CredentialedDrillConfig,
credentialed_adapter_smoke_report,
credentialed_drill_config_from_env,
missing_credentialed_adapter_env,
)
from .evaluation import EVALUATION_REPORT_SCHEMA, EVALUATION_TREND_SCHEMA, evaluation_threshold_report, evaluation_trend_artifact
from .external_adapters import (
ADAPTER_PACK_MANIFEST_SCHEMA,
ExternalAdapterPack,
@@ -76,6 +84,7 @@ from .retrieval import (
select_event_path,
)
from .service import LocalServiceRunner, RuntimeAdapterBundle, RuntimeConfig, health_report, resolve_runtime_adapters, runtime_from_config, service_contracts
from .service_app import SERVICE_APP_SCHEMA, ServiceAppConfig, build_service_binding, create_wsgi_app, service_app_metadata
from .service_binding import READINESS_REPORT_SCHEMA, SERVICE_BINDING_SCHEMA, ServiceBinding, ServiceResponse, service_binding_from_config
from .planner import plan_profile_execution
from .runtime import PhaseMemoryRuntime
@@ -83,9 +92,13 @@ from .runtime import PhaseMemoryRuntime
__all__ = [
"ActivationPlan",
"ADAPTER_PACK_MANIFEST_SCHEMA",
"CREDENTIALED_ADAPTER_ENV_VARS",
"CREDENTIALED_DRILL_SCHEMA",
"CredentialedDrillConfig",
"Diagnostic",
"ExternalAdapterPack",
"EVALUATION_REPORT_SCHEMA",
"EVALUATION_TREND_SCHEMA",
"FakeExternalEventLog",
"FakeExternalGraphStore",
"FakeExternalPolicyGateway",
@@ -130,8 +143,11 @@ __all__ = [
"branch_path",
"compact_path",
"create_path",
"credentialed_adapter_smoke_report",
"credentialed_drill_config_from_env",
"graph_from_markitect",
"evaluation_threshold_report",
"evaluation_trend_artifact",
"merge_path",
"make_review_record",
"plan_activation",
@@ -147,6 +163,7 @@ __all__ = [
"fake_external_adapter_pack",
"fake_external_runtime_config",
"live_shaped_adapter_pack",
"missing_credentialed_adapter_env",
"adapter_pack_manifest",
"validate_adapter_pack_manifest",
"path_event",
@@ -162,12 +179,17 @@ __all__ = [
"RuntimeAdapterBundle",
"READINESS_REPORT_SCHEMA",
"SERVICE_BINDING_SCHEMA",
"SERVICE_APP_SCHEMA",
"ServiceBinding",
"ServiceAppConfig",
"ServiceResponse",
"build_service_binding",
"create_wsgi_app",
"health_report",
"resolve_runtime_adapters",
"runtime_from_config",
"service_binding_from_config",
"service_app_metadata",
"service_contracts",
]

View File

@@ -16,6 +16,7 @@ LOCAL_STORE_MIGRATION_PLAN_SCHEMA = "phase_memory.local_store.migration_plan.v1"
LOCAL_STORE_MIGRATION_RESULT_SCHEMA = "phase_memory.local_store.migration_result.v1"
AUDIT_EXPORT_BATCH_SCHEMA = "phase_memory.audit.export_batch.v1"
AUDIT_RETENTION_PLAN_SCHEMA = "phase_memory.audit.retention_plan.v1"
AUDIT_RETENTION_RESULT_SCHEMA = "phase_memory.audit.retention_result.v1"
class InMemoryMemoryGraphStore:
@@ -435,6 +436,11 @@ class RecordingAuditSink:
def retention_plan(self, *, retention_days: int | None = None, now: datetime | None = None) -> dict[str, Any]:
return audit_retention_plan(self.events, retention_days=retention_days, now=now, retention=self.retention_metadata())
def apply_retention_plan(self, plan: dict[str, Any]) -> dict[str, Any]:
result, retained = audit_retention_apply(self.events, plan, retention=self.retention_metadata())
self.events = retained
return result
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())
@@ -471,6 +477,15 @@ class JsonlAuditSink:
def retention_plan(self, *, retention_days: int | None = None, now: datetime | None = None) -> dict[str, Any]:
return audit_retention_plan(self.query(), retention_days=retention_days, now=now, retention=self.retention_metadata())
def apply_retention_plan(self, plan: dict[str, Any]) -> dict[str, Any]:
result, retained = audit_retention_apply(self.query(), plan, retention=self.retention_metadata())
tmp_path = self.path.with_name(f".{self.path.name}.tmp")
with tmp_path.open("w", encoding="utf-8") as handle:
for event in retained:
handle.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
tmp_path.replace(self.path)
return result
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())
@@ -600,6 +615,34 @@ def audit_retention_plan(
}
def audit_retention_apply(
events: list[dict[str, Any]],
plan: dict[str, Any],
*,
retention: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
eligible = {str(item) for item in plan.get("eligible_operation_ids", ())}
retained_events: list[dict[str, Any]] = []
pruned: list[str] = []
for event in events:
event_id = str(event.get("operation_id") or event.get("id") or stable_digest(event))
if event_id in eligible:
pruned.append(event_id)
else:
retained_events.append(dict(event))
result = {
"schema_version": AUDIT_RETENTION_RESULT_SCHEMA,
"plan_id": str(plan.get("id") or ""),
"applied": True,
"changed": bool(pruned),
"pruned_count": len(pruned),
"retained_count": len(retained_events),
"pruned_operation_ids": sorted(pruned),
"retention": dict(retention or plan.get("retention") or {}),
}
return result, retained_events
def _audit_event_matches(event: dict[str, Any], filters: dict[str, Any]) -> bool:
operation = filters.get("operation")
if operation is not None and event.get("operation") != operation:

View File

@@ -0,0 +1,86 @@
"""Credential-gated adapter drill helpers.
The helpers in this module never store or print credentials. They validate that
an operator supplied the expected environment contract, then run the same local
manifest/conformance path used by live-shaped adapter fixtures.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping
from .external_adapters import live_shaped_adapter_pack, validate_adapter_pack_manifest
from .service import default_conformance_adapters
from .utils import stable_digest
CREDENTIALED_DRILL_SCHEMA = "phase_memory.credentialed_adapter_drill.v1"
CREDENTIALED_ADAPTER_ENV_VARS = (
"PHASE_MEMORY_MARKITECT_URL",
"PHASE_MEMORY_MARKITECT_TOKEN",
"PHASE_MEMORY_KONTEXTUAL_URL",
"PHASE_MEMORY_KONTEXTUAL_TOKEN",
)
@dataclass(frozen=True)
class CredentialedDrillConfig:
markitect_url: str
kontextual_url: str
def to_dict(self) -> dict[str, str]:
return {
"markitect_url": self.markitect_url,
"kontextual_url": self.kontextual_url,
"credential_fingerprint": stable_digest([self.markitect_url, self.kontextual_url]),
}
def missing_credentialed_adapter_env(environ: Mapping[str, str] | None = None) -> tuple[str, ...]:
environ = environ or {}
return tuple(name for name in CREDENTIALED_ADAPTER_ENV_VARS if not environ.get(name))
def credentialed_drill_config_from_env(environ: Mapping[str, str] | None = None) -> CredentialedDrillConfig:
environ = environ or {}
missing = missing_credentialed_adapter_env(environ)
if missing:
raise ValueError(f"Missing credentialed adapter drill environment: {', '.join(missing)}")
return CredentialedDrillConfig(
markitect_url=str(environ["PHASE_MEMORY_MARKITECT_URL"]),
kontextual_url=str(environ["PHASE_MEMORY_KONTEXTUAL_URL"]),
)
def credentialed_adapter_smoke_report(environ: Mapping[str, str] | None = None) -> dict:
environ = environ or {}
missing = missing_credentialed_adapter_env(environ)
if missing:
return {
"schema_version": CREDENTIALED_DRILL_SCHEMA,
"valid": False,
"skipped": True,
"missing_env": list(missing),
"diagnostics": [
{
"severity": "warn",
"code": "credential_env_missing",
"message": "Credentialed adapter drill skipped because required environment variables are missing.",
"metadata": {"required_env": list(CREDENTIALED_ADAPTER_ENV_VARS), "missing_env": list(missing)},
}
],
}
config = credentialed_drill_config_from_env(environ)
pack = live_shaped_adapter_pack()
diagnostics = validate_adapter_pack_manifest(pack)
conformance_adapters = default_conformance_adapters()
return {
"schema_version": CREDENTIALED_DRILL_SCHEMA,
"valid": not any(diagnostic.severity == "error" for diagnostic in diagnostics),
"skipped": False,
"config": config.to_dict(),
"adapter_pack": pack.manifest(),
"conformance_helpers": sorted(conformance_adapters),
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
}

View File

@@ -10,8 +10,10 @@ from .contracts import graph_from_markitect
from .models import Diagnostic, MemoryPath
from .retrieval import activation_quality_report, select_event_path
from .runtime import PhaseMemoryRuntime
from .utils import stable_digest, utc_now_iso
EVALUATION_REPORT_SCHEMA = "phase_memory.evaluation.threshold_report.v1"
EVALUATION_TREND_SCHEMA = "phase_memory.evaluation.trend_artifact.v1"
DEFAULT_THRESHOLDS = {
"policy_denial_count": 1,
@@ -65,6 +67,54 @@ def evaluation_threshold_report(data: dict[str, Any], *, thresholds: dict[str, f
}
def evaluation_trend_artifact(
report: dict[str, Any],
*,
previous_report: dict[str, Any] | None = None,
run_metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
run_metadata = {
"created_at": utc_now_iso(),
**dict(run_metadata or {}),
}
metrics = dict(report.get("metrics") or {})
thresholds = dict(report.get("thresholds") or {})
previous_metrics = dict((previous_report or {}).get("metrics") or {})
threshold_deltas = {
key: round(float(metrics.get(key) or 0) - float(threshold), 4)
for key, threshold in sorted(thresholds.items())
}
metric_deltas = {
key: round(float(value or 0) - float(previous_metrics.get(key) or 0), 4)
for key, value in sorted(metrics.items())
if key in previous_metrics
}
diagnostics = [dict(item) for item in report.get("diagnostics", ())]
for key, delta in metric_deltas.items():
if delta < 0:
diagnostics.append(
Diagnostic(
"warn",
"evaluation_metric_regressed",
"Evaluation metric declined from the previous report.",
key,
{"delta": delta, "current": metrics.get(key), "previous": previous_metrics.get(key)},
).to_dict()
)
artifact_id = f"evaluation-trend:{stable_digest([run_metadata, metrics, thresholds, previous_metrics])}"
return {
"schema_version": EVALUATION_TREND_SCHEMA,
"id": artifact_id,
"valid": not any(item.get("severity") == "error" for item in diagnostics),
"run": run_metadata,
"metrics": metrics,
"thresholds": thresholds,
"threshold_deltas": threshold_deltas,
"metric_deltas": metric_deltas,
"report": report,
"previous_report_id": (previous_report or {}).get("id", ""),
"diagnostics": diagnostics,
}
def _policy_scenario(scenario: dict[str, Any]) -> dict[str, Any]:
runtime = PhaseMemoryRuntime()
response = runtime.plan_activation(

View File

@@ -142,6 +142,13 @@ class FakeTelemetryAuditSink:
def retention_plan(self, *, retention_days: int | None = None, now=None) -> dict[str, Any]:
return audit_retention_plan(self.events, retention_days=retention_days, now=now, retention=self.retention_metadata())
def apply_retention_plan(self, plan: dict[str, Any]) -> dict[str, Any]:
from .adapters import audit_retention_apply
result, retained = audit_retention_apply(self.events, plan, retention=self.retention_metadata())
self.events = retained
return result
def export_batch(self, **filters: Any) -> dict[str, Any]:
events = self.query(**filters)
return audit_export_batch(events, filters=filters, retention=self.retention_metadata())

View File

@@ -24,6 +24,7 @@ class MemoryOperation(str, Enum):
AUDIT_QUERY = "audit.query"
AUDIT_EXPORT = "audit.export"
AUDIT_RETENTION_PLAN = "audit.retention.plan"
AUDIT_RETENTION_APPLY = "audit.retention.apply"
LIFECYCLE_APPLY = "lifecycle.apply"
STABILIZATION = "memory.stabilize"
COMPACTION = "memory.compact"

View File

@@ -40,6 +40,7 @@ RUNTIME_ENVELOPE_SCHEMA = "phase_memory.runtime.envelope.v1"
AUDIT_QUERY_SCHEMA = "phase_memory.audit.query.v1"
AUDIT_EXPORT_SCHEMA = "phase_memory.audit.export.v1"
AUDIT_RETENTION_SCHEMA = "phase_memory.audit.retention.v1"
AUDIT_RETENTION_APPLY_SCHEMA = "phase_memory.audit.retention.apply.v1"
PACKAGE_REQUEST_SCHEMA = MARKITECT_PACKAGE_REQUEST_SCHEMA
@@ -399,6 +400,62 @@ class PhaseMemoryRuntime:
"source": {"ref": source_ref},
}
def apply_audit_retention(
self,
retention_plan: dict[str, Any] | None = None,
*,
retention_days: int | None = None,
now: datetime | None = None,
source_ref: str = "audit",
) -> dict[str, Any]:
policy = self.policy_gateway.authorize(
action="audit.retention.apply",
resource="audit:events",
context={"source_ref": source_ref, "dry_run": False, "retention_days": retention_days},
)
diagnostics: tuple[Diagnostic, ...] = ()
if policy.allowed and hasattr(self.audit_sink, "apply_retention_plan"):
plan = retention_plan or self.audit_sink.retention_plan(retention_days=retention_days, now=now)
result = self.audit_sink.apply_retention_plan(plan)
elif policy.allowed:
plan = retention_plan or {}
result = {}
diagnostics = (
Diagnostic(
"error",
"audit_retention_apply_unsupported",
"Audit sink does not expose retention apply.",
self.audit_sink.__class__.__name__,
),
)
else:
plan = retention_plan or {}
result = {}
operation_id = f"op:{stable_digest(['audit.retention.apply', source_ref, retention_days, plan])}"
audit = self.audit_sink.record(
audit_event(
operation_id=operation_id,
operation="audit.retention.apply",
subject={"kind": "audit_events", "id": str(plan.get("id") or "retention")},
policy_decision=policy,
dry_run=False,
source_ref=source_ref,
)
)
return {
"schema_version": AUDIT_RETENTION_APPLY_SCHEMA,
"operation_id": operation_id,
"operation": "audit.retention.apply",
"dry_run": False,
"valid": policy.allowed and not any(diagnostic.severity == "error" for diagnostic in diagnostics),
"plan": plan,
"result": result,
"policy_decision": _policy_to_dict(policy),
"audit_receipt": audit,
"diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
"source": {"ref": source_ref},
}
def export_graph(self, *, graph_id: str = "local", source_ref: str = "local-store") -> dict[str, Any]:
events = self.event_log.list_events()
if hasattr(self.graph_store, "export_graph"):

View File

@@ -0,0 +1,77 @@
"""Deployable stdlib service entrypoint for phase-memory."""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from typing import Sequence
from wsgiref.simple_server import make_server
from .service import RuntimeConfig
from .service_binding import ServiceBinding, service_binding_from_config
SERVICE_APP_SCHEMA = "phase_memory.service.app.v1"
@dataclass(frozen=True)
class ServiceAppConfig:
host: str = "127.0.0.1"
port: int = 8080
local_store_path: str = ".phase-memory-local"
def runtime_config(self) -> RuntimeConfig:
return RuntimeConfig(local_store_path=self.local_store_path)
def to_dict(self) -> dict:
return {
"host": self.host,
"port": self.port,
"local_store_path": self.local_store_path,
}
def build_service_binding(config: ServiceAppConfig | None = None) -> ServiceBinding:
config = config or ServiceAppConfig()
return service_binding_from_config(config.runtime_config())
def service_app_metadata(config: ServiceAppConfig | None = None) -> dict:
config = config or ServiceAppConfig()
binding = build_service_binding(config)
readiness = binding.readiness()
return {
"schema_version": SERVICE_APP_SCHEMA,
"config": config.to_dict(),
"readiness": readiness,
"routes": {
"health": "/health",
"readiness": "/ready",
"contracts": "/contracts",
"operations": "/operations/{operation}",
},
}
def create_wsgi_app(config: ServiceAppConfig | None = None):
return build_service_binding(config).as_wsgi_app()
def serve(config: ServiceAppConfig | None = None) -> None:
config = config or ServiceAppConfig()
app = create_wsgi_app(config)
with make_server(config.host, config.port, app) as server:
server.serve_forever()
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Run the phase-memory service binding.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--store", default=".phase-memory-local")
parser.add_argument("--check", action="store_true", help="Build the app and return readiness status without listening.")
args = parser.parse_args(list(argv) if argv is not None else None)
config = ServiceAppConfig(host=args.host, port=args.port, local_store_path=args.store)
if args.check:
return 0 if service_app_metadata(config)["readiness"]["ok"] else 1
serve(config)
return 0