generated from coulomb/repo-seed
Implement credentialed drill packaging workplan
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
86
src/phase_memory/credentialed_drills.py
Normal file
86
src/phase_memory/credentialed_drills.py
Normal 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],
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"):
|
||||
|
||||
77
src/phase_memory/service_app.py
Normal file
77
src/phase_memory/service_app.py
Normal 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
|
||||
Reference in New Issue
Block a user