feat: ship portable Actors ontology v0.1
Browse files
portable_ontology/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Portable semantic ontology release contracts."""
|
portable_ontology/actors.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validation for the clean Actors-family ontology release.
|
| 2 |
+
|
| 3 |
+
This module validates release artifacts and inventory candidates. It does not
|
| 4 |
+
rewrite an existing graph or admit semantic records; admission remains with the
|
| 5 |
+
instance human gate.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
MANIFEST_PATH = Path(__file__).parent / "manifests" / "actors_family_v0_1.json"
|
| 17 |
+
CANONICAL_ACTOR_TYPES = ("person", "organization", "agent")
|
| 18 |
+
TAXONOMY_FIELDS = ("phylum", "class", "order", "genus", "species")
|
| 19 |
+
LEGACY_AGENT_NODE_TYPES = {
|
| 20 |
+
"agent_instance",
|
| 21 |
+
"agent_taxon",
|
| 22 |
+
"agent_phylum",
|
| 23 |
+
"agent_class",
|
| 24 |
+
"agent_order",
|
| 25 |
+
"agent_genus",
|
| 26 |
+
"agent_family",
|
| 27 |
+
"agent_species",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_manifest(path: Path = MANIFEST_PATH) -> dict[str, Any]:
|
| 32 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def manifest_digest(manifest: dict[str, Any]) -> str:
|
| 36 |
+
unsigned = json.loads(json.dumps(manifest))
|
| 37 |
+
unsigned["integrity"]["manifest_sha256"] = None
|
| 38 |
+
canonical = json.dumps(
|
| 39 |
+
unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
| 40 |
+
).encode("utf-8")
|
| 41 |
+
return hashlib.sha256(canonical).hexdigest()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
|
| 45 |
+
errors: list[str] = []
|
| 46 |
+
if manifest.get("schema_version") != "mycelium-portable-ontology/v0.1":
|
| 47 |
+
errors.append("unexpected schema_version")
|
| 48 |
+
if manifest.get("release_id") != "actors_family_v0_1":
|
| 49 |
+
errors.append("unexpected release_id")
|
| 50 |
+
actors = manifest.get("actors_family", {})
|
| 51 |
+
if tuple(actors.get("canonical_types", ())) != CANONICAL_ACTOR_TYPES:
|
| 52 |
+
errors.append("Actors canonical types must be exactly person, organization, agent")
|
| 53 |
+
noncanonical = set(actors.get("noncanonical_agent_node_types", ()))
|
| 54 |
+
if not {"provider", "model", "lane", "persona", "instance", "agent_instance", "agent_taxon"} <= noncanonical:
|
| 55 |
+
errors.append("agent facet/noncanonical-type boundary is incomplete")
|
| 56 |
+
|
| 57 |
+
card = manifest.get("agent_registration_card", {})
|
| 58 |
+
if card.get("is_actor_type") is not False:
|
| 59 |
+
errors.append("registration card must not become a fourth Actors type")
|
| 60 |
+
if tuple(card.get("required_taxonomy_fields", ())) != TAXONOMY_FIELDS:
|
| 61 |
+
errors.append("registration card taxonomy must be phylum/class/order/genus/species")
|
| 62 |
+
if "Do not copy legacy agent_* taxonomy nodes" not in card.get("legacy_import_rule", ""):
|
| 63 |
+
errors.append("legacy taxonomy-node exclusion is missing")
|
| 64 |
+
|
| 65 |
+
relations = manifest.get("required_agent_topology", {}).get("relations", {})
|
| 66 |
+
organization = relations.get("organization", {})
|
| 67 |
+
registration = relations.get("registration_card", {})
|
| 68 |
+
lineage = relations.get("lineage", {})
|
| 69 |
+
if organization.get("minimum") != 1 or organization.get("evidence_backed") is not True:
|
| 70 |
+
errors.append("every agent must have an evidence-backed organization relation")
|
| 71 |
+
if registration.get("minimum") != 1 or registration.get("maximum") != 1:
|
| 72 |
+
errors.append("every agent must instantiate exactly one registration card")
|
| 73 |
+
if registration.get("evidence_backed") is not True:
|
| 74 |
+
errors.append("registration-card relation must be evidence-backed")
|
| 75 |
+
if lineage.get("relation_type") != "subagent_of" or lineage.get("queryable") is not True:
|
| 76 |
+
errors.append("parent/subagent lineage must be explicit and queryable")
|
| 77 |
+
|
| 78 |
+
growth = manifest.get("growth_admission", {})
|
| 79 |
+
if growth.get("instance_human_gate_is_only_admission_authority") is not True:
|
| 80 |
+
errors.append("instance human gate must remain the only admission authority")
|
| 81 |
+
if growth.get("credentials_are_content") is not False:
|
| 82 |
+
errors.append("credentials must not be ontology content")
|
| 83 |
+
if growth.get("cross_instance_publication_does_not_canonicalize_destination") is not True:
|
| 84 |
+
errors.append("cross-instance publication boundary is missing")
|
| 85 |
+
|
| 86 |
+
expected = manifest_digest(manifest)
|
| 87 |
+
if manifest.get("integrity", {}).get("manifest_sha256") != expected:
|
| 88 |
+
errors.append("integrity.manifest_sha256 digest mismatch")
|
| 89 |
+
return errors
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def validate_agent_candidate(candidate: dict[str, Any]) -> list[str]:
|
| 93 |
+
"""Validate a clean-instance agent inventory row before gate admission.
|
| 94 |
+
|
| 95 |
+
Relations are represented by IDs here so an inventory diff can use the
|
| 96 |
+
same contract without inventing or mutating graph edges.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
errors: list[str] = []
|
| 100 |
+
if candidate.get("type") != "agent":
|
| 101 |
+
if candidate.get("type") in LEGACY_AGENT_NODE_TYPES:
|
| 102 |
+
errors.append("legacy agent taxonomy node types are not canonical")
|
| 103 |
+
else:
|
| 104 |
+
errors.append("candidate type must be agent")
|
| 105 |
+
return errors
|
| 106 |
+
|
| 107 |
+
agent_id = candidate.get("id")
|
| 108 |
+
if not isinstance(agent_id, str) or not agent_id.strip():
|
| 109 |
+
errors.append("agent id is required")
|
| 110 |
+
organizations = candidate.get("organization_ids")
|
| 111 |
+
if not isinstance(organizations, list) or not organizations or not all(
|
| 112 |
+
isinstance(value, str) and value.strip() for value in organizations
|
| 113 |
+
):
|
| 114 |
+
errors.append("at least one organization_id is required")
|
| 115 |
+
|
| 116 |
+
card = candidate.get("registration_card")
|
| 117 |
+
if not isinstance(card, dict):
|
| 118 |
+
errors.append("one registration_card is required")
|
| 119 |
+
else:
|
| 120 |
+
missing = [field for field in TAXONOMY_FIELDS if not isinstance(card.get(field), str) or not card[field].strip()]
|
| 121 |
+
if missing:
|
| 122 |
+
errors.append("registration_card missing taxonomy fields: " + ", ".join(missing))
|
| 123 |
+
|
| 124 |
+
lineage_kind = candidate.get("lineage_kind")
|
| 125 |
+
parent_id = candidate.get("parent_agent_id")
|
| 126 |
+
if lineage_kind not in {"root", "subagent"}:
|
| 127 |
+
errors.append("lineage_kind must be root or subagent")
|
| 128 |
+
elif lineage_kind == "root" and parent_id is not None:
|
| 129 |
+
errors.append("root agent must not have parent_agent_id")
|
| 130 |
+
elif lineage_kind == "subagent" and (not isinstance(parent_id, str) or not parent_id.strip()):
|
| 131 |
+
errors.append("subagent requires parent_agent_id")
|
| 132 |
+
return errors
|
portable_ontology/manifests/actors_family_v0_1.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "mycelium-portable-ontology/v0.1",
|
| 3 |
+
"release_id": "actors_family_v0_1",
|
| 4 |
+
"release_status": "canonical_portable_release",
|
| 5 |
+
"released_at": "2026-08-17",
|
| 6 |
+
"scope": "Clean Blank Mycelium and Noam Spore instances only",
|
| 7 |
+
"actors_family": {
|
| 8 |
+
"canonical_types": [
|
| 9 |
+
"person",
|
| 10 |
+
"organization",
|
| 11 |
+
"agent"
|
| 12 |
+
],
|
| 13 |
+
"definitions": {
|
| 14 |
+
"person": "One identifiable human being only.",
|
| 15 |
+
"organization": "An organization actor; product, model, agent, and organization identities remain distinct.",
|
| 16 |
+
"agent": "Any named AI operational identity that can participate in work or provenance."
|
| 17 |
+
},
|
| 18 |
+
"type_exclusions": {
|
| 19 |
+
"person": [
|
| 20 |
+
"AI or computational agent",
|
| 21 |
+
"organization",
|
| 22 |
+
"role",
|
| 23 |
+
"audience segment",
|
| 24 |
+
"fictional persona"
|
| 25 |
+
],
|
| 26 |
+
"agent": [
|
| 27 |
+
"human being",
|
| 28 |
+
"organization",
|
| 29 |
+
"role",
|
| 30 |
+
"product",
|
| 31 |
+
"unnamed model class"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"noncanonical_agent_node_types": [
|
| 35 |
+
"provider",
|
| 36 |
+
"model",
|
| 37 |
+
"lane",
|
| 38 |
+
"persona",
|
| 39 |
+
"instance",
|
| 40 |
+
"agent_instance",
|
| 41 |
+
"agent_taxon"
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
"agent_registration_card": {
|
| 45 |
+
"record_kind": "reusable_registration_card",
|
| 46 |
+
"is_actor_type": false,
|
| 47 |
+
"required_taxonomy_fields": [
|
| 48 |
+
"phylum",
|
| 49 |
+
"class",
|
| 50 |
+
"order",
|
| 51 |
+
"genus",
|
| 52 |
+
"species"
|
| 53 |
+
],
|
| 54 |
+
"reuse_rule": "One registration card may be instantiated by multiple individually tracked agents.",
|
| 55 |
+
"legacy_import_rule": "Do not copy legacy agent_* taxonomy nodes from Adi's live graph; map reviewed values into registration-card fields."
|
| 56 |
+
},
|
| 57 |
+
"required_agent_topology": {
|
| 58 |
+
"individually_trackable": true,
|
| 59 |
+
"relations": {
|
| 60 |
+
"organization": {
|
| 61 |
+
"relation_type": "associated_with_organization",
|
| 62 |
+
"target_type": "organization",
|
| 63 |
+
"minimum": 1,
|
| 64 |
+
"evidence_backed": true
|
| 65 |
+
},
|
| 66 |
+
"registration_card": {
|
| 67 |
+
"relation_type": "instantiates_registration_card",
|
| 68 |
+
"target_record_kind": "reusable_registration_card",
|
| 69 |
+
"minimum": 1,
|
| 70 |
+
"maximum": 1,
|
| 71 |
+
"evidence_backed": true
|
| 72 |
+
},
|
| 73 |
+
"lineage": {
|
| 74 |
+
"relation_type": "subagent_of",
|
| 75 |
+
"target_type": "agent",
|
| 76 |
+
"rule": "Every agent declares lineage_kind=root or lineage_kind=subagent; subagents require exactly one parent agent and roots require none.",
|
| 77 |
+
"queryable": true
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"facets_preserved": [
|
| 81 |
+
"provider",
|
| 82 |
+
"model",
|
| 83 |
+
"lane",
|
| 84 |
+
"persona",
|
| 85 |
+
"instance"
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
"growth_admission": {
|
| 89 |
+
"credentials_are_content": false,
|
| 90 |
+
"user_authorized_provider_credentials_may_open_bounded_candidate_lanes": true,
|
| 91 |
+
"agents_may_propose_names_and_registration_cards": true,
|
| 92 |
+
"instance_human_gate_is_only_admission_authority": true,
|
| 93 |
+
"cross_instance_publication_does_not_canonicalize_destination": true
|
| 94 |
+
},
|
| 95 |
+
"source_evidence": [
|
| 96 |
+
"ev_146b4d8547ed4585a80c07b313e39f6d",
|
| 97 |
+
"ev_afe89570b16740d8b078832a634e7ee9",
|
| 98 |
+
"ev_8a025c5dd6304957b019b172276dec83",
|
| 99 |
+
"ev_d99208305c6e46c59d7f75c3954c36f1",
|
| 100 |
+
"ev_dd7a27dbad984e4db32fad69767c06c4"
|
| 101 |
+
],
|
| 102 |
+
"explicit_exclusions": [
|
| 103 |
+
"No Adi live-graph mutation",
|
| 104 |
+
"No legacy agent_* taxonomy-node import",
|
| 105 |
+
"No provider credentials or secrets",
|
| 106 |
+
"No automatic cross-instance canonicalization",
|
| 107 |
+
"No rewrite of existing noncompliant graphs"
|
| 108 |
+
],
|
| 109 |
+
"integrity": {
|
| 110 |
+
"canonicalization": "UTF-8 JSON, recursively sorted object keys, compact separators, SHA-256; manifest_sha256 excludes itself",
|
| 111 |
+
"manifest_sha256": "90904b4e4c18de6b482e47d49cdcd7d7cecbf0e798caa407f965c16edf5ff1c2"
|
| 112 |
+
}
|
| 113 |
+
}
|
test_actors_ontology.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import unittest
|
| 3 |
+
|
| 4 |
+
from portable_ontology.actors import (
|
| 5 |
+
LEGACY_AGENT_NODE_TYPES,
|
| 6 |
+
load_manifest,
|
| 7 |
+
manifest_digest,
|
| 8 |
+
validate_agent_candidate,
|
| 9 |
+
validate_manifest,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
VALID_ROOT = {
|
| 14 |
+
"id": "agent_example_001",
|
| 15 |
+
"type": "agent",
|
| 16 |
+
"organization_ids": ["org_example"],
|
| 17 |
+
"registration_card": {
|
| 18 |
+
"phylum": "ExampleProvider",
|
| 19 |
+
"class": "API",
|
| 20 |
+
"order": "weaver",
|
| 21 |
+
"genus": "shuttle",
|
| 22 |
+
"species": "example",
|
| 23 |
+
},
|
| 24 |
+
"lineage_kind": "root",
|
| 25 |
+
"parent_agent_id": None,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ActorsOntologyTests(unittest.TestCase):
|
| 30 |
+
def test_release_manifest_and_digest(self):
|
| 31 |
+
manifest = load_manifest()
|
| 32 |
+
self.assertEqual([], validate_manifest(manifest))
|
| 33 |
+
self.assertEqual(manifest_digest(manifest), manifest["integrity"]["manifest_sha256"])
|
| 34 |
+
|
| 35 |
+
def test_valid_root_and_subagent(self):
|
| 36 |
+
self.assertEqual([], validate_agent_candidate(VALID_ROOT))
|
| 37 |
+
child = copy.deepcopy(VALID_ROOT)
|
| 38 |
+
child.update(id="agent_example_002", lineage_kind="subagent", parent_agent_id="agent_example_001")
|
| 39 |
+
self.assertEqual([], validate_agent_candidate(child))
|
| 40 |
+
|
| 41 |
+
def test_registration_card_can_be_reused_by_distinct_agents(self):
|
| 42 |
+
first = copy.deepcopy(VALID_ROOT)
|
| 43 |
+
second = copy.deepcopy(VALID_ROOT)
|
| 44 |
+
second["id"] = "agent_example_002"
|
| 45 |
+
self.assertEqual(first["registration_card"], second["registration_card"])
|
| 46 |
+
self.assertEqual([], validate_agent_candidate(first))
|
| 47 |
+
self.assertEqual([], validate_agent_candidate(second))
|
| 48 |
+
|
| 49 |
+
def test_missing_required_wiring_fails_closed(self):
|
| 50 |
+
for field in ("organization_ids", "registration_card", "lineage_kind"):
|
| 51 |
+
with self.subTest(field=field):
|
| 52 |
+
candidate = copy.deepcopy(VALID_ROOT)
|
| 53 |
+
candidate.pop(field)
|
| 54 |
+
self.assertTrue(validate_agent_candidate(candidate))
|
| 55 |
+
|
| 56 |
+
def test_legacy_taxonomy_node_types_are_rejected(self):
|
| 57 |
+
for legacy_type in LEGACY_AGENT_NODE_TYPES:
|
| 58 |
+
with self.subTest(legacy_type=legacy_type):
|
| 59 |
+
candidate = copy.deepcopy(VALID_ROOT)
|
| 60 |
+
candidate["type"] = legacy_type
|
| 61 |
+
self.assertIn(
|
| 62 |
+
"legacy agent taxonomy node types are not canonical",
|
| 63 |
+
validate_agent_candidate(candidate),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def test_manifest_contains_no_legacy_taxonomy_seed_ids(self):
|
| 67 |
+
manifest_text = __import__("json").dumps(load_manifest())
|
| 68 |
+
for prefix in ("agent_species_", "agent_genus_", "agent_order_", "agent_phylum_", "agent_class_"):
|
| 69 |
+
self.assertNotIn(prefix, manifest_text)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
unittest.main()
|