diff --git a/.github/workflows/scenario-learning-binding.yml b/.github/workflows/scenario-learning-binding.yml new file mode 100644 index 0000000..d97bd0f --- /dev/null +++ b/.github/workflows/scenario-learning-binding.yml @@ -0,0 +1,34 @@ +name: scenario-learning-binding + +on: + pull_request: + paths: + - "schemas/scenario-learning-proposal-binding.schema.json" + - "examples/scenario-learning/scenario-learning-proposal-binding.example.json" + - "scripts/validate_scenario_learning_proposal_binding.py" + - ".github/workflows/scenario-learning-binding.yml" + - "README.md" + - "Makefile" + push: + branches: + - main + paths: + - "schemas/scenario-learning-proposal-binding.schema.json" + - "examples/scenario-learning/scenario-learning-proposal-binding.example.json" + - "scripts/validate_scenario_learning_proposal_binding.py" + - ".github/workflows/scenario-learning-binding.yml" + - "README.md" + - "Makefile" + +jobs: + validate-scenario-learning-binding: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install validation dependency + run: python -m pip install jsonschema + - name: Validate ScenarioLearningProposalBinding example + run: make validate-scenario-learning-binding diff --git a/Makefile b/Makefile index 404d2e6..22c53db 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PYTHON ?= python -.PHONY: validate-upstreams validate-python validate-deploy-assets validate-agent-learning-proposal validate-governed-learning-lifecycle validate-workspace-recall-promotion validate local-preflight local-up local-smoke local-debug local-down +.PHONY: validate-upstreams validate-python validate-deploy-assets validate-agent-learning-proposal validate-scenario-learning-binding validate-governed-learning-lifecycle validate-workspace-recall-promotion validate local-preflight local-up local-smoke local-debug local-down validate-upstreams: $(PYTHON) scripts/validate_upstreams.py third_party/upstreams.lock.yaml @@ -17,6 +17,9 @@ validate-agent-learning-proposal: $(PYTHON) scripts/validate_agent_learning_proposal.py $(PYTHON) scripts/validate_agent_learning_proposal_generator.py +validate-scenario-learning-binding: + $(PYTHON) scripts/validate_scenario_learning_proposal_binding.py + validate-governed-learning-lifecycle: $(PYTHON) scripts/validate_governed_learning_lifecycle.py diff --git a/README.md b/README.md index 509b9a4..9086cb0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,28 @@ The workflow `.github/workflows/agent-learning-proposal.yml` runs this validatio The example enforces review-only proposal mode, pending human review, no raw sensitive payload storage, evidence references, policy decision references, repo-local operating-contract destinations, and disabled durable writeback. +## Scenario learning proposal bindings + +Memory Mesh now carries a narrow binding contract for durable learning candidates that originate from governed SCOPE-D adversarial scenarios. + +This binding does not write memory. It links a SCOPE-D scenario, runtime decision receipts, evidence references, policy decision references, and memory effect metadata to a review-only learning proposal. The purpose is to preserve scenario-derived lessons without allowing scenario output, ATT&CK coverage, dashboard exports, or model summaries to become durable memory by accident. + +The contract, example, validator, and workflow live at: + +- `schemas/scenario-learning-proposal-binding.schema.json` +- `examples/scenario-learning/scenario-learning-proposal-binding.example.json` +- `scripts/validate_scenario_learning_proposal_binding.py` +- `.github/workflows/scenario-learning-binding.yml` + +Validate locally: + +```bash +python -m pip install jsonschema +make validate-scenario-learning-binding +``` + +The example enforces review-only mode, pending human review, no raw sensitive payload storage, scenario/evidence/policy references, required runtime decision receipt references, pending review memory effects, and disabled durable writeback. + ## Lampstand adapter-record promotion packets Memory Mesh now carries a review-only promotion-packet contract for Lampstand governed adapter records. diff --git a/examples/scenario-learning/scenario-learning-proposal-binding.example.json b/examples/scenario-learning/scenario-learning-proposal-binding.example.json new file mode 100644 index 0000000..916460d --- /dev/null +++ b/examples/scenario-learning/scenario-learning-proposal-binding.example.json @@ -0,0 +1,47 @@ +{ + "apiVersion": "memory.mesh.scenario-learning-binding/v1", + "kind": "ScenarioLearningProposalBinding", + "bindingId": "urn:srcos:memory-binding:scenario-learning:workspace-transduction-001", + "createdAt": "2026-05-29T13:30:00Z", + "sourceScenario": { + "scenarioRef": "adversarial-scenario:workspace-transduction-001", + "scenarioClass": "workspace_transduction", + "sourceRepo": "SocioProphet/SCOPE-D", + "sourcePrRef": "SocioProphet/SCOPE-D#60", + "runtimeDecisionReceiptRefs": ["wargames-runtime-receipt:allow-validate-001"], + "claimPromotionState": "triaged", + "redactionState": "redacted" + }, + "agentLearningProposalRef": "urn:srcos:memory-proposal:scenario-workspace-transduction-001", + "memoryEffect": { + "effectType": "learning_candidate", + "targetRef": "memory-target:scope-d-wargames-lessons", + "proposalRequired": true, + "reviewState": "pending_review" + }, + "reviewOnlyGate": { + "proposalMode": "review_only", + "reviewRequired": true, + "reviewStatus": "pending", + "abstentionBoundary": "Do not write durable memory from SCOPE-D scenario output until the linked AgentLearningProposal is reviewed and explicitly approved by the later governance flow." + }, + "evidenceRefs": [ + "evidence:scout-header-summary-001", + "negative-evidence:missing-validator-record-001", + "proof:scout:profile-001" + ], + "policyDecisionRefs": [ + "policy:wargames-runtime-dry-run-v0.1", + "engagement-authorization:e6-tabletop-001", + "control:scenario-boundary-review" + ], + "redaction": { + "rawSensitivePayloadStored": false, + "redactionSummary": "Only scenario, evidence, proof, policy, and receipt references are stored. No raw transcript, credentials, payload bodies, or private telemetry are stored." + }, + "writeback": { + "enabled": false, + "performed": false, + "writebackRef": null + } +} diff --git a/schemas/scenario-learning-proposal-binding.schema.json b/schemas/scenario-learning-proposal-binding.schema.json new file mode 100644 index 0000000..2b2c008 --- /dev/null +++ b/schemas/scenario-learning-proposal-binding.schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.socioprophet.org/memory-mesh/scenario-learning-proposal-binding.schema.json", + "title": "Memory Mesh Scenario Learning Proposal Binding", + "type": "object", + "required": [ + "apiVersion", + "kind", + "bindingId", + "createdAt", + "sourceScenario", + "agentLearningProposalRef", + "memoryEffect", + "reviewOnlyGate", + "evidenceRefs", + "policyDecisionRefs", + "redaction", + "writeback" + ], + "properties": { + "apiVersion": { "const": "memory.mesh.scenario-learning-binding/v1" }, + "kind": { "const": "ScenarioLearningProposalBinding" }, + "bindingId": { "type": "string", "pattern": "^urn:srcos:memory-binding:scenario-learning:[a-z0-9][a-z0-9._:-]*$" }, + "createdAt": { "type": "string", "format": "date-time" }, + "sourceScenario": { + "type": "object", + "required": ["scenarioRef", "scenarioClass", "sourceRepo", "sourcePrRef", "runtimeDecisionReceiptRefs", "claimPromotionState", "redactionState"], + "properties": { + "scenarioRef": { "type": "string", "pattern": "^adversarial-scenario:[a-z0-9][a-z0-9._:-]*$" }, + "scenarioClass": { "type": "string", "minLength": 1 }, + "sourceRepo": { "type": "string", "minLength": 1 }, + "sourcePrRef": { "type": ["string", "null"] }, + "runtimeDecisionReceiptRefs": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^wargames-runtime-receipt:[a-z0-9][a-z0-9._:-]*$" } }, + "claimPromotionState": { "enum": ["raw", "hypothesis", "triaged", "blocked"] }, + "redactionState": { "enum": ["redacted", "synthetic", "withheld"] } + }, + "additionalProperties": false + }, + "agentLearningProposalRef": { "type": "string", "minLength": 1 }, + "memoryEffect": { + "type": "object", + "required": ["effectType", "targetRef", "proposalRequired", "reviewState"], + "properties": { + "effectType": { "enum": ["learning_candidate", "false_continuity", "stale_policy_resurrection", "entity_merge_error", "identity_split", "malicious_summary", "privilege_hidden_in_context", "poisoned_embedding", "sandbox_to_canon_leak", "invalid_claim_promotion"] }, + "targetRef": { "type": "string", "minLength": 1 }, + "proposalRequired": { "const": true }, + "reviewState": { "enum": ["pending_review", "blocked"] } + }, + "additionalProperties": false + }, + "reviewOnlyGate": { + "type": "object", + "required": ["proposalMode", "reviewRequired", "reviewStatus", "abstentionBoundary"], + "properties": { + "proposalMode": { "const": "review_only" }, + "reviewRequired": { "const": true }, + "reviewStatus": { "const": "pending" }, + "abstentionBoundary": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "evidenceRefs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "policyDecisionRefs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "redaction": { + "type": "object", + "required": ["rawSensitivePayloadStored", "redactionSummary"], + "properties": { + "rawSensitivePayloadStored": { "const": false }, + "redactionSummary": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "writeback": { + "type": "object", + "required": ["enabled", "performed", "writebackRef"], + "properties": { + "enabled": { "const": false }, + "performed": { "const": false }, + "writebackRef": { "type": "null" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/scripts/validate_scenario_learning_proposal_binding.py b/scripts/validate_scenario_learning_proposal_binding.py new file mode 100644 index 0000000..e5517bd --- /dev/null +++ b/scripts/validate_scenario_learning_proposal_binding.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Validate ScenarioLearningProposalBinding examples.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = ROOT / "schemas" / "scenario-learning-proposal-binding.schema.json" +DEFAULT_EXAMPLE = ROOT / "examples" / "scenario-learning" / "scenario-learning-proposal-binding.example.json" + + +def load_json(path: Path): + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def validate_binding(example: dict, *, source_label: str) -> int: + gate = example["reviewOnlyGate"] + if gate["proposalMode"] != "review_only": + print(f"{source_label}: Scenario learning binding must remain review_only.") + return 1 + + if gate["reviewRequired"] is not True or gate["reviewStatus"] != "pending": + print(f"{source_label}: Scenario learning binding must remain pending human review.") + return 1 + + writeback = example["writeback"] + if writeback["enabled"] is not False or writeback["performed"] is not False: + print(f"{source_label}: Scenario learning binding must not enable or perform durable writeback.") + return 1 + + if writeback["writebackRef"] is not None: + print(f"{source_label}: Scenario learning binding must not carry a writebackRef before approval.") + return 1 + + if example["redaction"]["rawSensitivePayloadStored"] is not False: + print(f"{source_label}: Scenario learning binding must not store raw sensitive payloads.") + return 1 + + scenario = example["sourceScenario"] + if scenario["claimPromotionState"] not in {"raw", "hypothesis", "triaged", "blocked"}: + print(f"{source_label}: Scenario claimPromotionState must not be promoted beyond triaged/blocked.") + return 1 + + if not scenario["runtimeDecisionReceiptRefs"]: + print(f"{source_label}: Scenario learning binding requires runtime decision receipt references.") + return 1 + + memory_effect = example["memoryEffect"] + if memory_effect["proposalRequired"] is not True: + print(f"{source_label}: Scenario memory effect must require proposal routing.") + return 1 + + if memory_effect["reviewState"] not in {"pending_review", "blocked"}: + print(f"{source_label}: Scenario memory effect must remain pending_review or blocked.") + return 1 + + if not example.get("evidenceRefs"): + print(f"{source_label}: Scenario learning binding requires evidenceRefs.") + return 1 + + if not example.get("policyDecisionRefs"): + print(f"{source_label}: Scenario learning binding requires policyDecisionRefs.") + return 1 + + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Validate ScenarioLearningProposalBinding artifact.") + parser.add_argument("--binding", default=str(DEFAULT_EXAMPLE), help="Scenario learning binding JSON file to validate") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + binding_path = Path(args.binding).resolve() + schema = load_json(SCHEMA) + example = load_json(binding_path) + Draft202012Validator.check_schema(schema) + validator = Draft202012Validator(schema) + errors = sorted(validator.iter_errors(example), key=lambda error: list(error.path)) + if errors: + print("Scenario learning proposal binding failed validation:") + for error in errors: + location = ".".join(str(part) for part in error.path) or "" + print(f" - {location}: {error.message}") + return 1 + + result = validate_binding(example, source_label=str(binding_path)) + if result != 0: + return result + + print(f"Scenario learning proposal binding validates against schema: {binding_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())