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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,43 @@ Agentplane treats execution as evidence-producing work. The current public evide
- `NetworkDoorPlanEvidence`
- `ExternalModelProviderRouteEvidence`
- `NativeAssistantBridgeEvidence`
- `SupplyChainValidationArtifact`

The Network Door / BYOM / Native Assistant evidence types are non-mutating by default. They record policy posture, references, route decisions, hash-only prompt/destination evidence, and side-effect flags without directly mutating firewall state, installing mesh components, contacting model providers, invoking native assistant APIs, or storing credentials.

---

## Prophet Trust Chain supply-chain validation

Agentplane owns the validation, replay, and receipt evidence slice of Prophet Trust Chain. The platform standard and admission contract live in `SocioProphet/prophet-platform`:

- `docs/standards/PROPHET_TRUST_CHAIN_V0.md`
- `docs/TRUST_CHAIN_ADMISSION_CONTRACT.md`
- `docs/standards/PROPHET_TRUST_CHAIN_IMPLEMENTATION_MAP.md`

The first Agentplane Trust Chain slice defines `SupplyChainValidationArtifact`, which binds runtime supply-chain evidence to Agentplane validation/replay/receipt evidence.

Relevant files:

- `schemas/trust-chain/supply-chain-validation-artifact.v0.1.schema.json`
- `tests/fixtures/trust-chain/supply-chain-validation.valid.json`
- `tests/fixtures/trust-chain/supply-chain-validation.blocked.json`
- `tools/validate_trust_chain_supply_chain_validation.py`
- `tools/tests/test_trust_chain_supply_chain_validation.py`

Validation:

```bash
python3 tools/validate_trust_chain_supply_chain_validation.py
python3 -m pytest -q tools/tests/test_trust_chain_supply_chain_validation.py
```

The valid fixture requires SBOM, VEX, lockfile, signature, scan, promotion, rollback, policy, guardrail, validation, replay, and runtime receipt references before production-scope execution and promotion are allowed. The blocked fixture denies execution and promotion, requires repair and human review, and preserves remediation authority.

Boundary: Agentplane records validation, replay, and runtime receipt evidence. It does not perform live package scanning, certify runtime production readiness by itself, replace Lattice Forge runtime evidence, replace Policy Fabric policy profiles, or replace Guardrail Fabric action admission.

---

## Prerequisites

| Tool | Purpose |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.socioprophet.dev/agentplane/trust-chain/supply-chain-validation-artifact.v0.1.schema.json",
"title": "SupplyChainValidationArtifact v0.1",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "artifact_id", "artifact_type", "runtime_asset_ref", "scope", "evidence_refs", "policy_decision_ref", "guardrail_decision_ref", "validation_artifact_ref", "replay_artifact_ref", "runtime_receipt_ref", "decision", "effects", "non_claims"],
"properties": {
"schema_version": {"const": "0.1"},
"artifact_id": {"type": "string", "pattern": "^agentplane:supply-chain-validation-artifact:"},
"artifact_type": {"const": "SupplyChainValidationArtifact"},
"runtime_asset_ref": {"type": "string", "pattern": "^runtime-asset://"},
"scope": {"type": "object", "additionalProperties": false, "required": ["environment", "risk_tier"], "properties": {"environment": {"enum": ["preview", "staging", "production"]}, "risk_tier": {"enum": ["internal", "enterprise", "regulated_enterprise"]}}},
"evidence_refs": {"type": "object", "additionalProperties": false, "required": ["sbom_ref", "vex_ref", "lockfile_ref", "signature_ref", "scan_record_ref"], "properties": {"sbom_ref": {"type": ["string", "null"]}, "vex_ref": {"type": ["string", "null"]}, "lockfile_ref": {"type": ["string", "null"]}, "signature_ref": {"type": ["string", "null"]}, "scan_record_ref": {"type": ["string", "null"]}, "promotion_evidence_ref": {"type": ["string", "null"]}, "rollback_evidence_ref": {"type": ["string", "null"]}}},
"policy_decision_ref": {"type": ["string", "null"]},
"guardrail_decision_ref": {"type": ["string", "null"]},
"validation_artifact_ref": {"type": ["string", "null"]},
"replay_artifact_ref": {"type": ["string", "null"]},
"runtime_receipt_ref": {"type": ["string", "null"]},
"decision": {"enum": ["validated", "blocked", "review_required"]},
"effects": {"type": "object", "additionalProperties": false, "required": ["execution_allowed", "promotion_allowed", "repair_required", "human_review_required"], "properties": {"execution_allowed": {"type": "boolean"}, "promotion_allowed": {"type": "boolean"}, "repair_required": {"type": "boolean"}, "human_review_required": {"type": "boolean"}}},
"remediation": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["step_id", "authority", "required_before_execution", "instruction"], "properties": {"step_id": {"type": "string"}, "authority": {"type": "string"}, "required_before_execution": {"type": "boolean"}, "instruction": {"type": "string"}}}},
"non_claims": {"type": "array", "items": {"type": "string"}, "minItems": 1}
}
}
49 changes: 49 additions & 0 deletions tests/fixtures/trust-chain/supply-chain-validation.blocked.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"schema_version": "0.1",
"artifact_id": "agentplane:supply-chain-validation-artifact:runtime-asset-demo-002",
"artifact_type": "SupplyChainValidationArtifact",
"runtime_asset_ref": "runtime-asset://lattice-forge/legacy-python-ml@0.0.3",
"scope": {
"environment": "production",
"risk_tier": "regulated_enterprise"
},
"evidence_refs": {
"sbom_ref": "sbom://lattice-forge/legacy-python-ml/0.0.3/spdx.json",
"vex_ref": null,
"lockfile_ref": "lockfile://lattice-forge/legacy-python-ml/0.0.3/flake.lock",
"signature_ref": "sigstore://lattice-forge/legacy-python-ml/0.0.3/bundle.sigstore",
"scan_record_ref": "scan://lattice-forge/legacy-python-ml/0.0.3/osv-scan.json",
"promotion_evidence_ref": null,
"rollback_evidence_ref": null
},
"policy_decision_ref": "policy://trust-chain/dependency-admission/regulated-enterprise-production",
"guardrail_decision_ref": "guardrail:decision:trust-chain-runtime-deny-001",
"validation_artifact_ref": null,
"replay_artifact_ref": null,
"runtime_receipt_ref": null,
"decision": "blocked",
"effects": {
"execution_allowed": false,
"promotion_allowed": false,
"repair_required": true,
"human_review_required": true
},
"remediation": [
{
"step_id": "patch-runtime-asset",
"authority": "SocioProphet/lattice-forge",
"required_before_execution": true,
"instruction": "Replace or patch the vulnerable RuntimeAsset and emit fresh SBOM, VEX, scan, signature, promotion, and rollback evidence."
},
{
"step_id": "rerun-agentplane-validation",
"authority": "SocioProphet/agentplane",
"required_before_execution": true,
"instruction": "Run validation and replay after the patched RuntimeAsset is available."
}
],
"non_claims": [
"This fixture is expected to remain blocked until required evidence exists.",
"This fixture does not perform live package scanning."
]
}
37 changes: 37 additions & 0 deletions tests/fixtures/trust-chain/supply-chain-validation.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"schema_version": "0.1",
"artifact_id": "agentplane:supply-chain-validation-artifact:runtime-asset-demo-001",
"artifact_type": "SupplyChainValidationArtifact",
"runtime_asset_ref": "runtime-asset://lattice-forge/prophet-python-ml@0.1.0",
"scope": {
"environment": "production",
"risk_tier": "regulated_enterprise"
},
"evidence_refs": {
"sbom_ref": "sbom://lattice-forge/prophet-python-ml/0.1.0/spdx.json",
"vex_ref": "vex://lattice-forge/prophet-python-ml/0.1.0/vex.json",
"lockfile_ref": "lockfile://lattice-forge/prophet-python-ml/0.1.0/flake.lock",
"signature_ref": "sigstore://lattice-forge/prophet-python-ml/0.1.0/bundle.sigstore",
"scan_record_ref": "scan://lattice-forge/prophet-python-ml/0.1.0/osv-scan.json",
"promotion_evidence_ref": "promotion://lattice-forge/prophet-python-ml/0.1.0/stable",
"rollback_evidence_ref": "rollback://lattice-forge/prophet-python-ml/0.0.1"
},
"policy_decision_ref": "policy://trust-chain/dependency-admission/regulated-enterprise-production",
"guardrail_decision_ref": "guardrail:decision:trust-chain-runtime-allow-001",
"validation_artifact_ref": "agentplane:validation:runtime-asset-demo-001",
"replay_artifact_ref": "agentplane:replay:runtime-asset-demo-001",
"runtime_receipt_ref": "runtime-receipt://trust-chain/demo-runtime-asset-001",
"decision": "validated",
"effects": {
"execution_allowed": true,
"promotion_allowed": true,
"repair_required": false,
"human_review_required": false
},
"remediation": [],
"non_claims": [
"This fixture does not perform live package scanning.",
"This fixture does not claim IBM/Red Hat Lightwell integration.",
"This fixture is a contract example and not a production certification by itself."
]
}
34 changes: 34 additions & 0 deletions tools/tests/test_trust_chain_supply_chain_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

import json
from pathlib import Path

from tools.validate_trust_chain_supply_chain_validation import main as validate_trust_chain_supply_chain_validation


ROOT = Path(__file__).resolve().parents[2]
VALID_FIXTURE = ROOT / "tests" / "fixtures" / "trust-chain" / "supply-chain-validation.valid.json"
BLOCKED_FIXTURE = ROOT / "tests" / "fixtures" / "trust-chain" / "supply-chain-validation.blocked.json"


def test_trust_chain_supply_chain_validation_artifacts_validate() -> None:
assert validate_trust_chain_supply_chain_validation() == 0


def test_valid_supply_chain_artifact_allows_execution_and_promotion() -> None:
fixture = json.loads(VALID_FIXTURE.read_text(encoding="utf-8"))
assert fixture["decision"] == "validated"
assert fixture["effects"]["execution_allowed"] is True
assert fixture["effects"]["promotion_allowed"] is True
assert fixture["effects"]["repair_required"] is False
assert fixture["effects"]["human_review_required"] is False


def test_blocked_supply_chain_artifact_blocks_execution_and_requires_repair() -> None:
fixture = json.loads(BLOCKED_FIXTURE.read_text(encoding="utf-8"))
assert fixture["decision"] == "blocked"
assert fixture["effects"]["execution_allowed"] is False
assert fixture["effects"]["promotion_allowed"] is False
assert fixture["effects"]["repair_required"] is True
assert fixture["effects"]["human_review_required"] is True
assert fixture["remediation"]
162 changes: 162 additions & 0 deletions tools/validate_trust_chain_supply_chain_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
SCHEMA = ROOT / "schemas" / "trust-chain" / "supply-chain-validation-artifact.v0.1.schema.json"
VALID_FIXTURE = ROOT / "tests" / "fixtures" / "trust-chain" / "supply-chain-validation.valid.json"
BLOCKED_FIXTURE = ROOT / "tests" / "fixtures" / "trust-chain" / "supply-chain-validation.blocked.json"

REQUIRED_PRODUCTION_REFS = {
"sbom_ref",
"vex_ref",
"lockfile_ref",
"signature_ref",
"scan_record_ref",
"promotion_evidence_ref",
"rollback_evidence_ref",
}


class ValidationError(Exception):
pass


def fail(message: str) -> None:
raise ValidationError(message)


def load_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise ValidationError(f"missing file: {path.relative_to(ROOT)}") from exc
except json.JSONDecodeError as exc:
raise ValidationError(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") from exc


def json_type_name(value: Any) -> str:
if value is None:
return "null"
if isinstance(value, bool):
return "boolean"
if isinstance(value, int) and not isinstance(value, bool):
return "integer"
if isinstance(value, float):
return "number"
if isinstance(value, str):
return "string"
if isinstance(value, list):
return "array"
if isinstance(value, dict):
return "object"
return type(value).__name__


def type_matches(value: Any, expected: str) -> bool:
actual = json_type_name(value)
if expected == "number":
return actual in {"integer", "number"}
return actual == expected


def validate_schema(schema: dict[str, Any], value: Any, path: str = "$") -> None:
if "const" in schema and value != schema["const"]:
fail(f"{path}: expected const {schema['const']!r}, got {value!r}")
if "enum" in schema and value not in schema["enum"]:
fail(f"{path}: {value!r} not in enum {schema['enum']!r}")
expected_type = schema.get("type")
if expected_type is not None:
expected_types = expected_type if isinstance(expected_type, list) else [expected_type]
if not any(type_matches(value, item) for item in expected_types):
fail(f"{path}: expected type {expected_types!r}, got {json_type_name(value)!r}")
if isinstance(value, dict):
required = schema.get("required", [])
for key in required:
if key not in value:
fail(f"{path}: missing required property {key!r}")
properties = schema.get("properties", {})
if schema.get("additionalProperties") is False:
extra = sorted(set(value) - set(properties))
if extra:
fail(f"{path}: unexpected properties {extra!r}")
additional = schema.get("additionalProperties")
for key, item in value.items():
child_schema = properties.get(key)
if child_schema is None and isinstance(additional, dict):
child_schema = additional
if child_schema is not None:
validate_schema(child_schema, item, f"{path}.{key}")
if isinstance(value, list):
item_schema = schema.get("items")
if item_schema is not None:
for index, item in enumerate(value):
validate_schema(item_schema, item, f"{path}[{index}]")


def validate_common(record: dict[str, Any], path: Path) -> None:
if not str(record.get("runtime_asset_ref", "")).startswith("runtime-asset://"):
fail(f"{path}: runtime_asset_ref must be runtime-asset://")
if record.get("scope", {}).get("environment") == "production" and record.get("scope", {}).get("risk_tier") == "regulated_enterprise":
if record.get("decision") == "validated":
refs = record.get("evidence_refs", {})
missing = sorted(key for key in REQUIRED_PRODUCTION_REFS if not refs.get(key))
if missing:
fail(f"{path}: validated production record missing refs: {missing}")
for ref_key in ("policy_decision_ref", "guardrail_decision_ref", "validation_artifact_ref", "replay_artifact_ref", "runtime_receipt_ref"):
if not record.get(ref_key):
fail(f"{path}: validated production record missing {ref_key}")


def validate_valid(record: dict[str, Any], path: Path) -> None:
validate_common(record, path)
if record.get("decision") != "validated":
fail(f"{path}: valid fixture must have decision=validated")
effects = record.get("effects", {})
if effects.get("execution_allowed") is not True or effects.get("promotion_allowed") is not True:
fail(f"{path}: valid fixture must allow execution and promotion")
if effects.get("repair_required") is not False or effects.get("human_review_required") is not False:
fail(f"{path}: valid fixture must not require repair or review")


def validate_blocked(record: dict[str, Any], path: Path) -> None:
validate_common(record, path)
if record.get("decision") != "blocked":
fail(f"{path}: blocked fixture must have decision=blocked")
effects = record.get("effects", {})
if effects.get("execution_allowed") is not False or effects.get("promotion_allowed") is not False:
fail(f"{path}: blocked fixture must deny execution and promotion")
if effects.get("repair_required") is not True or effects.get("human_review_required") is not True:
fail(f"{path}: blocked fixture must require repair and human review")
remediation = record.get("remediation", [])
if not isinstance(remediation, list) or len(remediation) < 1:
fail(f"{path}: blocked fixture requires remediation")
for item in remediation:
if item.get("required_before_execution") is not True:
fail(f"{path}: remediation must be required before execution")
if not item.get("authority"):
fail(f"{path}: remediation requires authority")


def main() -> int:
try:
schema = load_json(SCHEMA)
valid = load_json(VALID_FIXTURE)
blocked = load_json(BLOCKED_FIXTURE)
validate_schema(schema, valid)
validate_schema(schema, blocked)
validate_valid(valid, VALID_FIXTURE)
validate_blocked(blocked, BLOCKED_FIXTURE)
except ValidationError as exc:
print(f"ERR: {exc}", file=sys.stderr)
return 2
print("OK: Trust Chain supply-chain validation artifacts passed")
return 0


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