diff --git a/apps/api/cmd/socioprophet-api/main.go b/apps/api/cmd/socioprophet-api/main.go index ec55b313..bf663d41 100644 --- a/apps/api/cmd/socioprophet-api/main.go +++ b/apps/api/cmd/socioprophet-api/main.go @@ -85,65 +85,149 @@ func handleValidateChange(c net.Conn, env *tritrpcv1.Envelope, key [32]byte) { return } - response := map[string]any{ + response := buildValidateChangeResponse(req) + if err := writeJSONResponse(c, binding.ValidateChangeService, binding.ValidateChangeRes, response, key); err != nil { + log.Printf("write validate_change response: %v", err) + } +} + +func buildValidateChangeResponse(req map[string]any) map[string]any { + evidenceState := "missing_evidence" + evidenceStatus := "missing" + responseStatus := "environment_requested" + responseID := "environment:validate-change-v2-response:requested:api-stub" + receiptRefs := []string{} + receiptDigests := []string{} + runRefs := []string{} + failureCodes := []string{"validation_observation_missing"} + warnings := []string{"validation_observation_missing", "environment_execution_not_observed"} + nextAction := "agentplane_synthetic_sandbox_run" + executionStatus := "requested" + sandboxRunRef := "agentplane:sandbox-run:pending:api-stub" + evidenceRefs := []string{} + readinessState := "blocked" + mergeAllowed := false + blockingReasons := []string{"validation_observation_missing", "verified_receipt_required"} + readinessSummary := "Selected validation plans are not sufficient for PR readiness without observed receipt-backed evidence." + nonCertifiedClaims := []string{ + "Plans were selected but no execution evidence was observed.", + "No validation success is certified.", + "No merge readiness is certified.", + } + + if receipt, ok := req["exported_sociosphere_receipt"].(map[string]any); ok { + receiptID := stringValue(receipt, "receipt_id", "") + runRef := stringValue(receipt, "run_ref", "") + verification := mapValue(receipt, "verification") + verificationStatus := stringValue(verification, "status", "") + receiptRefs = appendIfNonEmpty(receiptRefs, receiptID) + runRefs = appendIfNonEmpty(runRefs, runRef) + if digest := digestString(receipt, "run_digest"); digest != "" { + receiptDigests = append(receiptDigests, digest) + } + sandboxRunRef = "agentplane:sandbox-run:exported-sociosphere-receipt" + evidenceRefs = appendIfNonEmpty(evidenceRefs, "evidence://sociosphere/svf/exported-receipt/"+receiptSuffix(receiptID)) + + switch verificationStatus { + case "verified": + evidenceState = "verified_receipt" + evidenceStatus = "observed" + responseStatus = "environment_observed" + responseID = "environment:validate-change-v2-response:observed:api-stub" + failureCodes = []string{} + warnings = []string{} + nextAction = "none_for_verified_receipt" + executionStatus = "observed" + readinessState = "ready" + mergeAllowed = true + blockingReasons = []string{} + readinessSummary = "Readiness is allowed only because a verified Sociosphere SVF receipt reference is present." + nonCertifiedClaims = []string{ + "Prophet Platform consumes the receipt but does not issue it.", + "Verified local receipt does not certify production readiness.", + } + case "failed": + evidenceState = "failed_receipt" + evidenceStatus = "failed" + responseStatus = "environment_failed" + responseID = "environment:validate-change-v2-response:failed:api-stub" + failureCodes = []string{"svf_receipt_failed"} + warnings = []string{"environment_validation_failed", "validation_receipt_failed"} + nextAction = "remediate_and_rerun_environment_validation" + executionStatus = "failed" + blockingReasons = []string{"svf_receipt_failed", "verified_receipt_required"} + readinessSummary = "Failed receipt evidence blocks PR readiness. Inspect diagnostics, patch the failure, and rerun the selected SVF plan before merge readiness can be claimed." + nonCertifiedClaims = []string{ + "Failed receipt blocks validation success.", + "Failed receipt does not certify production readiness.", + "Failed receipt requires repair and rerun before PR readiness.", + } + case "stale": + evidenceState = "stale_receipt" + evidenceStatus = "stale" + responseStatus = "environment_failed" + responseID = "environment:validate-change-v2-response:stale-receipt:api-stub" + failureCodes = []string{"svf_receipt_stale"} + warnings = []string{"validation_receipt_stale"} + nextAction = "rerun_selected_svf_plan" + executionStatus = "failed" + blockingReasons = []string{"svf_receipt_stale", "verified_receipt_required"} + readinessSummary = "Stale receipt evidence blocks PR readiness for the current change set. Rerun the selected SVF plan before readiness can be claimed." + nonCertifiedClaims = []string{ + "Stale receipt does not certify the current change set.", + "Stale receipt does not certify production readiness.", + "Stale receipt requires rerun before PR readiness.", + } + } + } + + evidenceSummary := map[string]any{ + "evidence_status": evidenceStatus, + "validation_evidence_state": evidenceState, + "receipt_refs": receiptRefs, + "receipt_digests": receiptDigests, + "run_refs": runRefs, + "failure_codes": failureCodes, + "non_certified_claims": nonCertifiedClaims, + } + + return map[string]any{ "schema_version": "1.0", "request_id": stringValue(req, "request_id", "environment:validate-change-v2-request:unknown"), - "response_id": "environment:validate-change-v2-response:requested:api-stub", - "status": "environment_requested", + "response_id": responseID, + "status": responseStatus, "repo": stringValue(req, "repo", "unknown/unknown"), "sociosphere_refs": req["sociosphere_refs"], "selected_plans": req["selected_plans"], "environment": req["environment_request"], "agentplane_execution": map[string]any{ "executor_plane": "AgentPlane", - "sandbox_run_ref": "agentplane:sandbox-run:pending:api-stub", - "execution_status": "requested", - "evidence_refs": []string{}, - }, - "evidence_summary": map[string]any{ - "evidence_status": "missing", - "validation_evidence_state": "missing_evidence", - "receipt_refs": []string{}, - "failure_codes": []string{ - "validation_observation_missing", - }, - "non_certified_claims": []string{ - "Plans were selected but no execution evidence was observed.", - "No validation success is certified.", - "No merge readiness is certified.", - }, + "sandbox_run_ref": sandboxRunRef, + "execution_status": executionStatus, + "evidence_refs": evidenceRefs, }, + "evidence_summary": evidenceSummary, "pr_readiness": map[string]any{ - "readiness_state": "blocked", - "merge_allowed": false, + "readiness_state": readinessState, + "merge_allowed": mergeAllowed, "required_evidence_state": "verified_receipt", - "observed_evidence_state": "missing_evidence", - "blocking_reason_codes": []string{ - "validation_observation_missing", - "verified_receipt_required", - }, - "summary": "Selected validation plans are not sufficient for PR readiness without observed receipt-backed evidence.", + "observed_evidence_state": evidenceState, + "blocking_reason_codes": blockingReasons, + "summary": readinessSummary, "non_claims": []string{ - "Readiness block is based on missing evidence state.", - "Readiness block does not execute validation.", + "Readiness is derived from exported Sociosphere receipt state.", + "Readiness block or allowance does not execute validation in Prophet Platform.", }, }, - "warnings": []string{ - "validation_observation_missing", - "environment_execution_not_observed", - }, - "next_required_action": "agentplane_synthetic_sandbox_run", + "warnings": warnings, + "next_required_action": nextAction, "non_claims": []string{ "API stub does not execute live sandbox infrastructure.", "API stub does not certify Signadot-style runtime parity.", - "API stub returns a deterministic environment_requested response only.", + "API stub consumes exported Sociosphere receipt state only.", "API stub cannot report merge readiness without verified receipt evidence.", }, } - - if err := writeJSONResponse(c, binding.ValidateChangeService, binding.ValidateChangeRes, response, key); err != nil { - log.Printf("write validate_change response: %v", err) - } } func writeJSONResponse(c net.Conn, service string, method string, payload any, key [32]byte) error { @@ -154,6 +238,40 @@ func writeJSONResponse(c net.Conn, service string, method string, payload any, k return binding.WriteRecord(c, respNonce, respFrame) } +func mapValue(m map[string]any, key string) map[string]any { + if value, ok := m[key].(map[string]any); ok { + return value + } + return map[string]any{} +} + +func digestString(m map[string]any, key string) string { + record := mapValue(m, key) + algorithm := stringValue(record, "algorithm", "") + digest := stringValue(record, "digest", "") + if algorithm == "" || digest == "" { + return "" + } + return algorithm + ":" + digest +} + +func appendIfNonEmpty(values []string, value string) []string { + if strings.TrimSpace(value) == "" { + return values + } + return append(values, value) +} + +func receiptSuffix(receiptID string) string { + suffix := strings.TrimPrefix(receiptID, "svf:receipt:") + suffix = strings.ReplaceAll(suffix, ":", "-") + suffix = strings.ReplaceAll(suffix, "/", "-") + if suffix == "" { + return "unknown" + } + return suffix +} + func stringValue(m map[string]any, key string, fallback string) string { if value, ok := m[key].(string); ok && strings.TrimSpace(value) != "" { return value diff --git a/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.example.json b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.example.json new file mode 100644 index 00000000..af3bea8a --- /dev/null +++ b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.example.json @@ -0,0 +1,83 @@ +{ + "schema_version": "1.0", + "request_id": "environment:validate-change-v2-request:sociosphere-exported-receipt", + "repo": "SocioProphet/sociosphere", + "ref": "pull/434/head", + "changed_paths": [ + "registry/sovereign-validation-fabric.yaml", + "tools/svf_runner.py" + ], + "change_digest": { + "algorithm": "sha256", + "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "sociosphere_refs": { + "workspace_registry": "svf:registry:sociosphere.workspace", + "environment_profile_id": "environment-sandbox:profile:sociosphere.control-plane-readout", + "workspace_validation_status": "verified_receipt", + "export_manifest_ref": "SocioProphet/sociosphere@7133223edd7784a36b15e3eee9065f17b49b5451:artifacts/svf/exports/latest/export-manifest.json" + }, + "selected_plans": [ + "svf:plan:sociosphere.registry-dogfood" + ], + "environment_request": { + "baseline_ref": "workspace://sociosphere/main", + "changed_service_refs": [], + "requested_isolation_class": "synthetic_no_network", + "requested_routing_class": "not_configured", + "requested_async_isolation_class": "not_configured", + "requested_stateful_resource_isolation_class": "not_configured" + }, + "execution": { + "executor_plane": "AgentPlane", + "evidence_required": true, + "requested_action": "consume_exported_sociosphere_receipt" + }, + "exported_sociosphere_receipt": { + "schema_version": "1.0", + "receipt_id": "svf:receipt:sociosphere.registry-dogfood.verified", + "run_ref": "svf:run:sociosphere.registry-dogfood.verified", + "export_manifest_ref": "SocioProphet/sociosphere@7133223edd7784a36b15e3eee9065f17b49b5451:artifacts/svf/exports/latest/export-manifest.json", + "run_digest": { + "algorithm": "sha256", + "digest": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "repo": "SocioProphet/sociosphere", + "profile_ref": "svf:profile:sociosphere.dogfood", + "plan_ref": "svf:plan:sociosphere.registry-dogfood", + "policy_ref": "svf:policy:sociosphere.local-readonly", + "verification": { + "status": "verified", + "verifier": "sociosphere.svf_runner.local", + "verified_at": "2026-05-31T18:46:09Z", + "diagnostics": [] + }, + "certified_claims": [ + "schema_conformant", + "non_production_only", + "policy_boundary_preserved", + "artifact_integrity_verified", + "receipt_integrity_verified" + ], + "non_certified_claims": [ + "production_readiness", + "live_infrastructure_safety", + "container_runtime_parity", + "browser_runtime_parity", + "qemu_runtime_parity", + "signadot_vendor_parity", + "network_isolation_enforced" + ] + }, + "expected_projection": { + "validation_evidence_state": "verified_receipt", + "merge_allowed": true, + "blocking_reason_codes": [] + }, + "non_claims": [ + "Request fixture supplies exported Sociosphere receipt evidence only.", + "Prophet Platform does not issue or sign the receipt.", + "Request fixture does not authorize production remediation.", + "Request fixture depends on the upstream Sociosphere export manifest merged in SocioProphet/sociosphere#456." + ] +} diff --git a/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.failed.example.json b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.failed.example.json new file mode 100644 index 00000000..81b90a71 --- /dev/null +++ b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.failed.example.json @@ -0,0 +1,82 @@ +{ + "schema_version": "1.0", + "request_id": "environment:validate-change-v2-request:sociosphere-exported-receipt-failed", + "repo": "SocioProphet/sociosphere", + "ref": "pull/434/head", + "changed_paths": [ + "registry/sovereign-validation-fabric.yaml", + "tools/svf_runner.py" + ], + "change_digest": { + "algorithm": "sha256", + "digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "sociosphere_refs": { + "workspace_registry": "svf:registry:sociosphere.workspace", + "environment_profile_id": "environment-sandbox:profile:sociosphere.control-plane-readout", + "workspace_validation_status": "failed_receipt" + }, + "selected_plans": [ + "svf:plan:sociosphere.registry-dogfood" + ], + "environment_request": { + "baseline_ref": "workspace://sociosphere/main", + "changed_service_refs": [], + "requested_isolation_class": "synthetic_no_network", + "requested_routing_class": "not_configured", + "requested_async_isolation_class": "not_configured", + "requested_stateful_resource_isolation_class": "not_configured" + }, + "execution": { + "executor_plane": "AgentPlane", + "evidence_required": true, + "requested_action": "consume_exported_sociosphere_receipt" + }, + "exported_sociosphere_receipt": { + "schema_version": "1.0", + "receipt_id": "svf:receipt:sociosphere.registry-dogfood.failed", + "run_ref": "svf:run:sociosphere.registry-dogfood.failed", + "run_digest": { + "algorithm": "sha256", + "digest": "6666666666666666666666666666666666666666666666666666666666666666" + }, + "repo": "SocioProphet/sociosphere", + "profile_ref": "svf:profile:sociosphere.dogfood", + "plan_ref": "svf:plan:sociosphere.registry-dogfood", + "policy_ref": "svf:policy:sociosphere.local-readonly", + "verification": { + "status": "failed", + "verifier": "sociosphere.svf_runner.local", + "verified_at": "2026-05-31T18:49:09Z", + "diagnostics": [ + "run_digest does not match run_artifact" + ] + }, + "certified_claims": [ + "non_production_only" + ], + "non_certified_claims": [ + "production_readiness", + "live_infrastructure_safety", + "container_runtime_parity", + "browser_runtime_parity", + "qemu_runtime_parity", + "signadot_vendor_parity", + "network_isolation_enforced", + "receipt_integrity_verified" + ] + }, + "expected_projection": { + "validation_evidence_state": "failed_receipt", + "merge_allowed": false, + "blocking_reason_codes": [ + "svf_receipt_failed", + "verified_receipt_required" + ] + }, + "non_claims": [ + "Request fixture supplies failed external Sociosphere receipt evidence only.", + "Prophet Platform does not repair the failed receipt.", + "Failed receipt must not authorize merge readiness." + ] +} diff --git a/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.stale.example.json b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.stale.example.json new file mode 100644 index 00000000..dc7f3bf1 --- /dev/null +++ b/contracts/environment/validate-change-v2-request.exported-sociosphere-receipt.stale.example.json @@ -0,0 +1,82 @@ +{ + "schema_version": "1.0", + "request_id": "environment:validate-change-v2-request:sociosphere-exported-receipt-stale", + "repo": "SocioProphet/sociosphere", + "ref": "pull/434/head", + "changed_paths": [ + "registry/sovereign-validation-fabric.yaml", + "tools/svf_runner.py" + ], + "change_digest": { + "algorithm": "sha256", + "digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "sociosphere_refs": { + "workspace_registry": "svf:registry:sociosphere.workspace", + "environment_profile_id": "environment-sandbox:profile:sociosphere.control-plane-readout", + "workspace_validation_status": "stale_receipt" + }, + "selected_plans": [ + "svf:plan:sociosphere.registry-dogfood" + ], + "environment_request": { + "baseline_ref": "workspace://sociosphere/main", + "changed_service_refs": [], + "requested_isolation_class": "synthetic_no_network", + "requested_routing_class": "not_configured", + "requested_async_isolation_class": "not_configured", + "requested_stateful_resource_isolation_class": "not_configured" + }, + "execution": { + "executor_plane": "AgentPlane", + "evidence_required": true, + "requested_action": "consume_exported_sociosphere_receipt" + }, + "exported_sociosphere_receipt": { + "schema_version": "1.0", + "receipt_id": "svf:receipt:sociosphere.registry-dogfood.stale", + "run_ref": "svf:run:sociosphere.registry-dogfood.stale", + "run_digest": { + "algorithm": "sha256", + "digest": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "repo": "SocioProphet/sociosphere", + "profile_ref": "svf:profile:sociosphere.dogfood", + "plan_ref": "svf:plan:sociosphere.registry-dogfood", + "policy_ref": "svf:policy:sociosphere.local-readonly", + "verification": { + "status": "stale", + "verifier": "sociosphere.svf_runner.local", + "verified_at": "2026-05-31T18:40:09Z", + "diagnostics": [ + "receipt does not match current change digest" + ] + }, + "certified_claims": [ + "non_production_only" + ], + "non_certified_claims": [ + "production_readiness", + "live_infrastructure_safety", + "container_runtime_parity", + "browser_runtime_parity", + "qemu_runtime_parity", + "signadot_vendor_parity", + "network_isolation_enforced", + "receipt_integrity_verified" + ] + }, + "expected_projection": { + "validation_evidence_state": "stale_receipt", + "merge_allowed": false, + "blocking_reason_codes": [ + "svf_receipt_stale", + "verified_receipt_required" + ] + }, + "non_claims": [ + "Request fixture supplies stale external Sociosphere receipt evidence only.", + "Prophet Platform does not refresh the stale receipt.", + "Stale receipt must not authorize merge readiness." + ] +} diff --git a/tools/smoke_validate_change_v2_api_stub.py b/tools/smoke_validate_change_v2_api_stub.py index 5943b656..60f2ddbb 100644 --- a/tools/smoke_validate_change_v2_api_stub.py +++ b/tools/smoke_validate_change_v2_api_stub.py @@ -9,6 +9,10 @@ API = ROOT / "apps" / "api" / "cmd" / "socioprophet-api" / "main.go" GATEWAY = ROOT / "apps" / "gateway" / "cmd" / "tritrpc-gateway" / "main.go" REQUEST = ROOT / "contracts" / "environment" / "validate-change-v2-request.example.json" +EXPORTED_RECEIPT_REQUEST = ROOT / "contracts" / "environment" / "validate-change-v2-request.exported-sociosphere-receipt.example.json" +FAILED_EXPORTED_RECEIPT_REQUEST = ROOT / "contracts" / "environment" / "validate-change-v2-request.exported-sociosphere-receipt.failed.example.json" +STALE_EXPORTED_RECEIPT_REQUEST = ROOT / "contracts" / "environment" / "validate-change-v2-request.exported-sociosphere-receipt.stale.example.json" +UPSTREAM_EXPORT_MANIFEST = "SocioProphet/sociosphere@7133223edd7784a36b15e3eee9065f17b49b5451:artifacts/svf/exports/latest/export-manifest.json" REQUIRED_BINDING = [ 'ValidateChangeService = "platform.validate_change.v2"', @@ -17,17 +21,25 @@ ] REQUIRED_API = [ "handleValidateChange", + "buildValidateChangeResponse", + "exported_sociosphere_receipt", "binding.ValidateChangeService", "binding.ValidateChangeReq", - '"status": "environment_requested"', + '"status": responseStatus', '"agentplane_synthetic_sandbox_run"', '"evidence_summary"', - '"validation_evidence_state": "missing_evidence"', + '"validation_evidence_state": evidenceState', + '"verified_receipt"', + '"failed_receipt"', + '"stale_receipt"', + '"run_refs": runRefs', + '"agentplane:sandbox-run:exported-sociosphere-receipt"', '"pr_readiness"', - '"merge_allowed": false', + '"merge_allowed": mergeAllowed', '"required_evidence_state": "verified_receipt"', '"verified_receipt_required"', '"API stub does not execute live sandbox infrastructure."', + '"API stub consumes exported Sociosphere receipt state only."', ] REQUIRED_GATEWAY = [ 'mux.HandleFunc("/v1/validate-change"', @@ -43,6 +55,33 @@ def require(text: str, needle: str, surface: str, problems: list[str]) -> None: problems.append(f"missing {needle!r} in {surface}") +def validate_exported_request(path: Path, expected_status: str, expected_state: str, expected_merge_allowed: bool, problems: list[str]) -> None: + data = json.loads(path.read_text(encoding="utf-8")) + receipt = data.get("exported_sociosphere_receipt", {}) + projection = data.get("expected_projection", {}) + label = str(path.relative_to(ROOT)) + + if receipt.get("verification", {}).get("status") != expected_status: + problems.append(f"{label}: expected verification.status {expected_status}") + if not str(receipt.get("receipt_id", "")).startswith("svf:receipt:"): + problems.append(f"{label}: expected svf:receipt id") + if not str(receipt.get("run_ref", "")).startswith("svf:run:"): + problems.append(f"{label}: expected svf:run ref") + if data.get("execution", {}).get("executor_plane") != "AgentPlane": + problems.append(f"{label}: executor_plane must be AgentPlane") + if projection.get("validation_evidence_state") != expected_state: + problems.append(f"{label}: expected projected state {expected_state}") + if projection.get("merge_allowed") is not expected_merge_allowed: + problems.append(f"{label}: expected merge_allowed {expected_merge_allowed}") + if expected_merge_allowed is False and "verified_receipt_required" not in projection.get("blocking_reason_codes", []): + problems.append(f"{label}: blocked projection must require verified_receipt") + if expected_status == "verified": + if receipt.get("export_manifest_ref") != UPSTREAM_EXPORT_MANIFEST: + problems.append(f"{label}: verified receipt must reference merged upstream export manifest") + if data.get("sociosphere_refs", {}).get("export_manifest_ref") != UPSTREAM_EXPORT_MANIFEST: + problems.append(f"{label}: sociosphere_refs.export_manifest_ref must reference merged upstream export manifest") + + def main() -> int: problems: list[str] = [] binding = BINDING.read_text(encoding="utf-8") @@ -62,6 +101,10 @@ def main() -> int: if request.get("environment_request", {}).get("requested_isolation_class") != "synthetic_no_network": problems.append("request fixture must remain synthetic_no_network") + validate_exported_request(EXPORTED_RECEIPT_REQUEST, "verified", "verified_receipt", True, problems) + validate_exported_request(FAILED_EXPORTED_RECEIPT_REQUEST, "failed", "failed_receipt", False, problems) + validate_exported_request(STALE_EXPORTED_RECEIPT_REQUEST, "stale", "stale_receipt", False, problems) + result = { "validator": "prophet-platform.validate-change-v2.api-stub-smoke.v1", "passed": not problems, @@ -69,7 +112,8 @@ def main() -> int: "non_claims": [ "Smoke check does not execute live sandbox infrastructure.", "Smoke check does not certify Signadot-style runtime parity.", - "Smoke check validates route/contract wiring and readiness-field presence only." + "Smoke check validates route/contract wiring and readiness-field presence only.", + "Smoke check validates exported Sociosphere receipt request fixture shape only." ] } print(json.dumps(result, indent=2, sort_keys=True))