Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build test validate validate-contracts validate-maturity validate-negative-fixtures dist release-dry-run clean
.PHONY: build test validate validate-contracts validate-maturity validate-negative-fixtures validate-wallguard-clean-room-synthesis dist release-dry-run clean

BIN := holmes
DIST_DIR := dist
Expand All @@ -22,6 +22,10 @@ validate-maturity:

validate-contracts:
python3 tools/validate_holmes.py
python3 tools/validate_wallguard_clean_room_synthesis.py

validate-wallguard-clean-room-synthesis:
python3 tools/validate_wallguard_clean_room_synthesis.py

validate-negative-fixtures:
python3 tools/run_negative_fixtures.py
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"apiVersion": "holmes.socioprophet.dev/v1",
"kind": "WallGuardCleanRoomSynthesis",
"metadata": {
"name": "wallguard-clean-room-synthesis-bad-clean-room-rejected",
"version": "0.1.0"
},
"spec": {
"synthesisId": "holmes-synthesis://clean-room/invalid-001",
"sourceRetrievalRefs": ["sherlock-retrieval-filter://same-wall-pre-rank-001"],
"sourceWallRefs": ["wall://client-a/matter-x"],
"destinationRef": "workroom://firm/approved-knowledge",
"destinationWallRef": "none",
"wallDecisionRef": "wallguard-decision://clean-room-invalid-001",
"wallDecisionOutcome": "clean_room_release_allowed",
"synthesisDecision": "clean_room_release",
"outputClassification": "firm_approved",
"sourceLabelPreserved": true,
"restrictedPayloadExcluded": false,
"residualRestrictions": [],
"receiptRefs": ["wallguard-receipt://clean-room-invalid-001"],
"evidenceRefs": ["SocioProphet/sherlock-search#61", "SocioProphet/guardrail-fabric#32"]
}
}
24 changes: 24 additions & 0 deletions examples/wallguard-clean-room-synthesis.cross-wall.rejected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"apiVersion": "holmes.socioprophet.dev/v1",
"kind": "WallGuardCleanRoomSynthesis",
"metadata": {
"name": "wallguard-clean-room-synthesis-cross-wall-rejected",
"version": "0.1.0"
},
"spec": {
"synthesisId": "holmes-synthesis://cross-wall/invalid-001",
"sourceRetrievalRefs": ["sherlock-retrieval-filter://cross-wall-invalid-001"],
"sourceWallRefs": ["wall://client-a/matter-x", "wall://client-b/matter-y"],
"destinationRef": "workroom://client-a/matter-x",
"destinationWallRef": "wall://client-a/matter-x",
"wallDecisionRef": "wallguard-decision://cross-wall-synthesis-invalid-001",
"wallDecisionOutcome": "allow",
"synthesisDecision": "same_wall_synthesis",
"outputClassification": "firm_approved",
"sourceLabelPreserved": true,
"restrictedPayloadExcluded": false,
"residualRestrictions": [],
"receiptRefs": ["wallguard-receipt://cross-wall-synthesis-invalid-001"],
"evidenceRefs": ["SocioProphet/sherlock-search#61"]
}
}
24 changes: 24 additions & 0 deletions examples/wallguard-clean-room-synthesis.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"apiVersion": "holmes.socioprophet.dev/v1",
"kind": "WallGuardCleanRoomSynthesis",
"metadata": {
"name": "wallguard-clean-room-synthesis-valid",
"version": "0.1.0"
},
"spec": {
"synthesisId": "holmes-synthesis://client-a/matter-x/clean-room-001",
"sourceRetrievalRefs": ["sherlock-retrieval-filter://same-wall-pre-rank-001"],
"sourceWallRefs": ["wall://client-a/matter-x"],
"destinationRef": "workroom://client-a/matter-x",
"destinationWallRef": "wall://client-a/matter-x",
"wallDecisionRef": "wallguard-decision://same-wall-synthesis-allow-001",
"wallDecisionOutcome": "allow",
"synthesisDecision": "same_wall_synthesis",
"outputClassification": "wall_restricted",
"sourceLabelPreserved": true,
"restrictedPayloadExcluded": false,
"residualRestrictions": ["wall://client-a/matter-x"],
"receiptRefs": ["wallguard-receipt://same-wall-synthesis-allow-001"],
"evidenceRefs": ["SocioProphet/sherlock-search#61", "SocioProphet/memory-mesh#33"]
}
}
62 changes: 62 additions & 0 deletions schemas/wallguard-clean-room-synthesis.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.socioprophet.org/holmes/wallguard-clean-room-synthesis.schema.json",
"title": "WallGuard Clean Room Synthesis",
"type": "object",
"additionalProperties": false,
"required": [
"apiVersion",
"kind",
"metadata",
"spec"
],
"properties": {
"apiVersion": {"const": "holmes.socioprophet.dev/v1"},
"kind": {"const": "WallGuardCleanRoomSynthesis"},
"metadata": {
"type": "object",
"additionalProperties": false,
"required": ["name", "version"],
"properties": {
"name": {"type": "string", "minLength": 1},
"version": {"type": "string", "minLength": 1}
}
},
"spec": {
"type": "object",
"additionalProperties": false,
"required": [
"synthesisId",
"sourceRetrievalRefs",
"sourceWallRefs",
"destinationRef",
"destinationWallRef",
"wallDecisionRef",
"wallDecisionOutcome",
"synthesisDecision",
"outputClassification",
"sourceLabelPreserved",
"restrictedPayloadExcluded",
"residualRestrictions",
"receiptRefs",
"evidenceRefs"
],
"properties": {
"synthesisId": {"type": "string", "minLength": 1},
"sourceRetrievalRefs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true},
"sourceWallRefs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true},
"destinationRef": {"type": "string", "minLength": 1},
"destinationWallRef": {"type": "string", "minLength": 1},
"wallDecisionRef": {"type": "string", "minLength": 1},
"wallDecisionOutcome": {"enum": ["allow", "deny", "redact", "quarantine", "escalate", "clean_room_release_requested", "clean_room_release_allowed", "clean_room_release_denied"]},
"synthesisDecision": {"enum": ["same_wall_synthesis", "deny", "redact", "quarantine", "clean_room_release"]},
"outputClassification": {"enum": ["wall_restricted", "clean_room_derived", "firm_approved", "public_sanitized", "quarantined"]},
"sourceLabelPreserved": {"const": true},
"restrictedPayloadExcluded": {"type": "boolean"},
"residualRestrictions": {"type": "array", "items": {"type": "string", "minLength": 1}},
"receiptRefs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true},
"evidenceRefs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
}
}
}
}
101 changes: 101 additions & 0 deletions tools/validate_wallguard_clean_room_synthesis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Validate WallGuard clean-room synthesis fixtures."""

from __future__ import annotations

import json
from pathlib import Path

from jsonschema import Draft202012Validator

ROOT = Path(__file__).resolve().parents[1]
SCHEMA = ROOT / "schemas" / "wallguard-clean-room-synthesis.schema.json"
VALID = ROOT / "examples" / "wallguard-clean-room-synthesis.valid.json"
REJECTED = [
ROOT / "examples" / "wallguard-clean-room-synthesis.cross-wall.rejected.json",
ROOT / "examples" / "wallguard-clean-room-synthesis.bad-clean-room.rejected.json",
]


def load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)


def validate_schema(instance: dict, schema: dict, *, source_label: str) -> None:
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(instance), key=lambda error: list(error.path))
if errors:
lines = [f"{source_label} failed schema validation:"]
for error in errors:
location = ".".join(str(part) for part in error.path) or "<root>"
lines.append(f" - {location}: {error.message}")
raise ValueError("\n".join(lines))


def semantic_diagnostics(record: dict) -> list[str]:
diagnostics: list[str] = []
spec = record["spec"]
source_walls = set(spec["sourceWallRefs"])
destination_wall = spec["destinationWallRef"]
outcome = spec["wallDecisionOutcome"]
decision = spec["synthesisDecision"]
classification = spec["outputClassification"]

if spec["sourceLabelPreserved"] is not True:
diagnostics.append("source labels must be preserved")

if decision == "same_wall_synthesis":
if outcome != "allow":
diagnostics.append("same-wall synthesis requires WallGuard allow outcome")
if len(source_walls) != 1:
diagnostics.append("same-wall synthesis requires exactly one source wall")
if destination_wall not in source_walls:
diagnostics.append("same-wall synthesis destination wall must match source wall")
if classification != "wall_restricted":
diagnostics.append("same-wall synthesis output must remain wall_restricted")
if not spec["residualRestrictions"]:
diagnostics.append("same-wall synthesis requires residual restrictions")

if decision == "clean_room_release":
if outcome != "clean_room_release_allowed":
diagnostics.append("clean-room release requires clean_room_release_allowed outcome")
if spec["restrictedPayloadExcluded"] is not True:
diagnostics.append("clean-room release must exclude restricted payload")
if classification not in {"clean_room_derived", "public_sanitized"}:
diagnostics.append("clean-room release output must be clean_room_derived or public_sanitized")
if not spec["residualRestrictions"]:
diagnostics.append("clean-room release must retain residual restrictions")

if decision in {"deny", "quarantine"} and outcome == "allow":
diagnostics.append("deny/quarantine synthesis cannot use allow outcome")

if classification in {"firm_approved", "public_sanitized"} and spec["restrictedPayloadExcluded"] is not True:
diagnostics.append("broader release classifications require restrictedPayloadExcluded=true")

return diagnostics


def check(path: Path, schema: dict, expected: str) -> dict:
record = load_json(path)
validate_schema(record, schema, source_label=str(path.relative_to(ROOT)))
diagnostics = semantic_diagnostics(record)
actual = "fail" if diagnostics else "pass"
result = {"example": path.name, "expected": expected, "actual": actual, "diagnostics": diagnostics}
if actual != expected:
raise ValueError(json.dumps(result, indent=2))
return result


def main() -> int:
schema = load_json(SCHEMA)
Draft202012Validator.check_schema(schema)
results = [check(VALID, schema, "pass")]
for path in REJECTED:
results.append(check(path, schema, "fail"))
print(json.dumps({"ok": True, "checked": results}, indent=2))
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading