clm-kernel 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"""VCard Model (Application Plane) - Implementation of Arena + Action.
|
|
2
|
+
|
|
3
|
+
This module defines the VCard, which represents the sovereign decision layer
|
|
4
|
+
in the MVP Cards architecture. VCard is the IO Monad that manages all side effects.
|
|
5
|
+
|
|
6
|
+
Categorical Role:
|
|
7
|
+
- Applicative Functor (<*>): Applies PCard functions with side effects.
|
|
8
|
+
- UPTV Role: Token/Evidence container and Gateway.
|
|
9
|
+
|
|
10
|
+
DOTS Vocabulary Role: Arena + Action
|
|
11
|
+
====================================
|
|
12
|
+
|
|
13
|
+
VCard is both:
|
|
14
|
+
- **Arena**: The interface type defining what can interact (subject_did, capabilities, external_refs)
|
|
15
|
+
- **Action**: The morphism where interactions (PCards) act on systems (MCards) to produce new systems
|
|
16
|
+
|
|
17
|
+
Empty Schema Principle:
|
|
18
|
+
=======================
|
|
19
|
+
VCard IS an MCard. It has no additional schema fields. Its "VCard-ness" comes
|
|
20
|
+
from its structured YAML content (Abstract/Concrete/Balanced logic mapped to policies).
|
|
21
|
+
|
|
22
|
+
The Four Roles (Content Sections):
|
|
23
|
+
1. Identity & Credential Container (The "Who") -> `identity`
|
|
24
|
+
2. Verification Hub (The "Rules") -> `verification`
|
|
25
|
+
3. Side Effect Manager (The "Bridge") -> `external_refs`
|
|
26
|
+
4. Input/Output Gatekeeper (The "Gate") -> `gatekeeper`
|
|
27
|
+
|
|
28
|
+
See Also:
|
|
29
|
+
- docs/VCard_Impl.md
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from enum import Enum
|
|
36
|
+
from typing import Any, cast
|
|
37
|
+
|
|
38
|
+
import yaml
|
|
39
|
+
|
|
40
|
+
from clm.layer0.model.card import MCard
|
|
41
|
+
from clm.layer0.model.dots import DOTSMetadata, create_vcard_dots_metadata
|
|
42
|
+
from clm.layer0.model.hash.validator import HashAlgorithm
|
|
43
|
+
from clm.layer0.model.pcard import PCard
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CapabilityScope(Enum):
|
|
47
|
+
"""Scope of a capability token."""
|
|
48
|
+
|
|
49
|
+
READ = "read"
|
|
50
|
+
WRITE = "write"
|
|
51
|
+
EXECUTE = "execute"
|
|
52
|
+
ADMIN = "admin"
|
|
53
|
+
DELEGATE = "delegate"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GatekeeperDirection(Enum):
|
|
57
|
+
"""Direction of gatekeeper authorization."""
|
|
58
|
+
|
|
59
|
+
INGRESS = "ingress" # Content entering the PKC
|
|
60
|
+
EGRESS = "egress" # Content leaving the PKC
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Capability:
|
|
65
|
+
"""A capability token defining authorized actions."""
|
|
66
|
+
|
|
67
|
+
capability_id: str
|
|
68
|
+
actor_did: str
|
|
69
|
+
scope: CapabilityScope
|
|
70
|
+
resource_pattern: str # Regex or glob pattern for resources
|
|
71
|
+
expires_at: datetime | None = None
|
|
72
|
+
constraints: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
transferable: bool = False
|
|
74
|
+
|
|
75
|
+
def is_valid(self) -> bool:
|
|
76
|
+
"""Check if the capability is still valid."""
|
|
77
|
+
if self.expires_at is None:
|
|
78
|
+
return True
|
|
79
|
+
# Simple naive check; in production use timezone-aware UTC
|
|
80
|
+
return datetime.now() < self.expires_at
|
|
81
|
+
|
|
82
|
+
def matches_resource(self, resource_hash: str) -> bool:
|
|
83
|
+
"""Check if this capability applies to a resource."""
|
|
84
|
+
import re
|
|
85
|
+
|
|
86
|
+
return bool(re.match(self.resource_pattern, resource_hash))
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict[str, Any]:
|
|
89
|
+
"""Serialize to dictionary."""
|
|
90
|
+
return {
|
|
91
|
+
"id": self.capability_id,
|
|
92
|
+
"actor": self.actor_did,
|
|
93
|
+
"scope": self.scope.value,
|
|
94
|
+
"resource_pattern": self.resource_pattern,
|
|
95
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
96
|
+
"transferable": self.transferable,
|
|
97
|
+
"constraints": self.constraints,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ExternalRef:
|
|
103
|
+
"""A verified external reference managed by VCard (IO Monad)."""
|
|
104
|
+
|
|
105
|
+
uri: str
|
|
106
|
+
content_hash: str
|
|
107
|
+
status: str # "verified", "pending", "stale", "invalid"
|
|
108
|
+
signature: str | None = None
|
|
109
|
+
last_verified: datetime | None = None
|
|
110
|
+
qos_metrics: dict[str, Any] = field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
"uri": self.uri,
|
|
115
|
+
"content_hash": self.content_hash,
|
|
116
|
+
"status": self.status,
|
|
117
|
+
"qos": self.qos_metrics,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class GatekeeperEvent:
|
|
123
|
+
"""An ingress or egress gatekeeper event."""
|
|
124
|
+
|
|
125
|
+
direction: GatekeeperDirection
|
|
126
|
+
timestamp: datetime
|
|
127
|
+
source_did: str | None # For ingress
|
|
128
|
+
destination_did: str | None # For egress
|
|
129
|
+
content_hash: str
|
|
130
|
+
authorized: bool
|
|
131
|
+
capability_used: str | None = None
|
|
132
|
+
signature: str | None = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Functional Helpers (The Preferred API)
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_vcard(card: MCard) -> bool:
|
|
141
|
+
"""Check if an MCard contains VCard content."""
|
|
142
|
+
try:
|
|
143
|
+
content = yaml.safe_load(card.get_content(as_text=True))
|
|
144
|
+
# Support both JSON-style (root keys) and VCard-root style
|
|
145
|
+
if isinstance(content, dict):
|
|
146
|
+
if (
|
|
147
|
+
"vcard" in content
|
|
148
|
+
and content["vcard"].get("type") == "authentication-authorization"
|
|
149
|
+
):
|
|
150
|
+
return True
|
|
151
|
+
if content.get("type") == "VCard": # Legacy/Direct JSON support
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
except Exception:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_vcard_content(
|
|
159
|
+
subject_did: str,
|
|
160
|
+
controller_pubkeys: list[str],
|
|
161
|
+
capabilities: list[Capability],
|
|
162
|
+
external_refs: list[ExternalRef],
|
|
163
|
+
pcard_refs: list[dict[str, Any]] | None = None,
|
|
164
|
+
gatekeeper_config: dict[str, Any] | None = None,
|
|
165
|
+
managed_event_types: list[str] | None = None,
|
|
166
|
+
wrapped_pcard_hash: str | None = None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Generate the standard VCard content structure."""
|
|
169
|
+
|
|
170
|
+
# Normalize pcard_refs if likely passed as list of strings or dicts
|
|
171
|
+
final_pcard_refs = pcard_refs or []
|
|
172
|
+
|
|
173
|
+
# Construct the canonical VCard YAML Structure as per docs/VCard_Impl.md
|
|
174
|
+
return {
|
|
175
|
+
"vcard": {
|
|
176
|
+
"version": "1.0",
|
|
177
|
+
"type": "authentication-authorization",
|
|
178
|
+
"identity": {
|
|
179
|
+
"subject_did": subject_did,
|
|
180
|
+
"controller_pubkeys": controller_pubkeys,
|
|
181
|
+
"issued_at": datetime.now(timezone.utc).isoformat(),
|
|
182
|
+
},
|
|
183
|
+
"verification": {"pcard_refs": final_pcard_refs},
|
|
184
|
+
# Map capability objects to list of dicts if needed, or store in gatekeeper
|
|
185
|
+
# Note: The doc puts capabilities in `gatekeeper.capabilities`
|
|
186
|
+
"gatekeeper": gatekeeper_config
|
|
187
|
+
or {
|
|
188
|
+
"ingress_policies": [],
|
|
189
|
+
"egress_policies": [],
|
|
190
|
+
"capabilities": [c.to_dict() for c in capabilities],
|
|
191
|
+
},
|
|
192
|
+
"external_refs": [r.to_dict() for r in external_refs],
|
|
193
|
+
"managed_event_types": managed_event_types or [],
|
|
194
|
+
"wrapped_pcard_hash": wrapped_pcard_hash,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_pcard_refs(vcard: MCard) -> list[str]:
|
|
200
|
+
"""Extract PCard reference hashes from VCard."""
|
|
201
|
+
content = _parse_vcard_content(vcard)
|
|
202
|
+
refs = content.get("verification", {}).get("pcard_refs", [])
|
|
203
|
+
# Return just hashes if they are dicts
|
|
204
|
+
return [r["hash"] if isinstance(r, dict) else r for r in refs]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_subject_did(vcard: MCard) -> str | None:
|
|
208
|
+
"""Extract subject DID from VCard."""
|
|
209
|
+
content = _parse_vcard_content(vcard)
|
|
210
|
+
return content.get("identity", {}).get("subject_did") or content.get("subject_did")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def create_vcard(
|
|
214
|
+
subject_did: str,
|
|
215
|
+
controller_pubkeys: list[str],
|
|
216
|
+
capabilities: list[Capability] | None = None,
|
|
217
|
+
external_refs: list[ExternalRef] | None = None,
|
|
218
|
+
managed_event_types: list[str] | None = None,
|
|
219
|
+
wrapped_pcard_hash: str | None = None,
|
|
220
|
+
) -> MCard:
|
|
221
|
+
"""Functional factory to create a VCard (returns VCard instance which IS MCard)."""
|
|
222
|
+
return VCard(
|
|
223
|
+
subject_did=subject_did,
|
|
224
|
+
controller_pubkeys=controller_pubkeys,
|
|
225
|
+
capabilities=capabilities,
|
|
226
|
+
external_refs=external_refs,
|
|
227
|
+
managed_event_types=managed_event_types,
|
|
228
|
+
wrapped_pcard_hash=wrapped_pcard_hash,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_vcard_content(card: MCard) -> dict[str, Any]:
|
|
233
|
+
"""Internal helper to parse VCard content, verifying structure."""
|
|
234
|
+
raw = yaml.safe_load(card.get_content(as_text=True))
|
|
235
|
+
if "vcard" in raw:
|
|
236
|
+
return raw["vcard"]
|
|
237
|
+
# Fallback to direct dict (Legacy/JSON compat)
|
|
238
|
+
return raw
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# =============================================================================
|
|
242
|
+
# VCard Class (MCard View / Wrapper)
|
|
243
|
+
# =============================================================================
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class VCard(PCard):
|
|
247
|
+
"""VCard - The Application Plane unit (Arena + Action).
|
|
248
|
+
|
|
249
|
+
Categorical Hierarchy: MCard (Monad) → PCard (Functor) → **VCard (Applicative)**
|
|
250
|
+
|
|
251
|
+
VCard inherits PCard's structured-content parsing and SMC combinators,
|
|
252
|
+
adding sovereign identity, capability-based authorization, gatekeeper
|
|
253
|
+
logic, and VCard Sandwich witness factories.
|
|
254
|
+
|
|
255
|
+
As the *guard* (precondition) layer for PCard, VCard overrides
|
|
256
|
+
``_validate_and_parse()`` to accept both JSON (VCard Sandwich pattern)
|
|
257
|
+
and YAML content, providing graceful fallbacks for non-dict payloads.
|
|
258
|
+
|
|
259
|
+
IMPORTANT: While MCard content is immutable (and thus the .hash attribute
|
|
260
|
+
refers to the INITIAL content), this Python object allows in-memory mutation
|
|
261
|
+
of capabilities and logs to support runtime Gatekeeper logic.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(
|
|
265
|
+
self,
|
|
266
|
+
subject_did: str | bytes,
|
|
267
|
+
controller_pubkeys: list[str] | None = None,
|
|
268
|
+
capabilities: list[Capability] | None = None,
|
|
269
|
+
external_refs: list[ExternalRef] | None = None,
|
|
270
|
+
managed_event_types: list[str] | None = None,
|
|
271
|
+
wrapped_pcard_hash: str | None = None,
|
|
272
|
+
hash_function: str | HashAlgorithm = "sha256",
|
|
273
|
+
**kwargs: Any,
|
|
274
|
+
):
|
|
275
|
+
"""Initialize a VCard.
|
|
276
|
+
|
|
277
|
+
This constructor is overloaded to support:
|
|
278
|
+
1. Creating NEW VCard: pass subject_did, keys, etc.
|
|
279
|
+
2. Wrapping EXISTING content: pass content as first arg.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# Check if we are creating from scratch or wrapping
|
|
283
|
+
is_creation = controller_pubkeys is not None
|
|
284
|
+
|
|
285
|
+
self._gatekeeper_log: list[GatekeeperEvent] = []
|
|
286
|
+
self._export_manifest: list[str] = []
|
|
287
|
+
|
|
288
|
+
if is_creation:
|
|
289
|
+
# Creation Mode
|
|
290
|
+
full_content_dict = create_vcard_content(
|
|
291
|
+
subject_did=cast(str, subject_did),
|
|
292
|
+
controller_pubkeys=controller_pubkeys or [],
|
|
293
|
+
capabilities=capabilities or [],
|
|
294
|
+
external_refs=external_refs or [],
|
|
295
|
+
managed_event_types=managed_event_types or [],
|
|
296
|
+
wrapped_pcard_hash=wrapped_pcard_hash,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Use inner dict for initial state
|
|
300
|
+
initial_data = full_content_dict["vcard"]
|
|
301
|
+
|
|
302
|
+
# Incorporate any additional kwargs directly into the vcard content
|
|
303
|
+
for k, v in kwargs.items():
|
|
304
|
+
initial_data[k] = v
|
|
305
|
+
|
|
306
|
+
# Serialize to JSON for MCard storage (Immutable Base)
|
|
307
|
+
final_content = json.dumps(full_content_dict, sort_keys=True)
|
|
308
|
+
super().__init__(final_content, hash_function)
|
|
309
|
+
else:
|
|
310
|
+
# Wrapping Mode (subject_did is actually content)
|
|
311
|
+
super().__init__(subject_did, hash_function)
|
|
312
|
+
initial_data = self._get_vcard_inner()
|
|
313
|
+
|
|
314
|
+
# Initialize Mutable State from Content
|
|
315
|
+
self._initialize_mutable_state(initial_data)
|
|
316
|
+
|
|
317
|
+
def get_wrapped_pcard_hash(self) -> str | None:
|
|
318
|
+
"""Get the explicitly wrapped PCard hash, if provided."""
|
|
319
|
+
return self._get_vcard_inner().get("wrapped_pcard_hash")
|
|
320
|
+
|
|
321
|
+
# =========================================================================
|
|
322
|
+
# VCard Guard: _validate_and_parse override (Precondition for PCard)
|
|
323
|
+
# =========================================================================
|
|
324
|
+
|
|
325
|
+
def _validate_and_parse(self) -> dict[str, Any]:
|
|
326
|
+
"""VCard guard override — parse JSON or YAML content gracefully.
|
|
327
|
+
|
|
328
|
+
This overrides PCard's strict YAML-only parser to serve as the
|
|
329
|
+
*precondition guard* for the MCard → PCard → VCard hierarchy:
|
|
330
|
+
|
|
331
|
+
1. JSON content (VCard Sandwich ``{"vcard": {...}}``) is parsed first.
|
|
332
|
+
2. YAML content is parsed as a fallback.
|
|
333
|
+
3. Non-dict content (e.g., ``null``, scalars) returns ``{}``
|
|
334
|
+
instead of raising, providing a graceful guard.
|
|
335
|
+
"""
|
|
336
|
+
content_str = self.get_content(as_text=True)
|
|
337
|
+
if not isinstance(content_str, str):
|
|
338
|
+
return {}
|
|
339
|
+
|
|
340
|
+
# 1. Try JSON (VCard Sandwich pattern)
|
|
341
|
+
stripped = content_str.strip()
|
|
342
|
+
if stripped.startswith("{"):
|
|
343
|
+
try:
|
|
344
|
+
parsed = json.loads(stripped)
|
|
345
|
+
if isinstance(parsed, dict):
|
|
346
|
+
return parsed
|
|
347
|
+
except (json.JSONDecodeError, ValueError):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
# 2. Try YAML
|
|
351
|
+
try:
|
|
352
|
+
parsed = yaml.safe_load(content_str)
|
|
353
|
+
if isinstance(parsed, dict):
|
|
354
|
+
return parsed
|
|
355
|
+
except yaml.YAMLError:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
# 3. Graceful fallback — non-dict content is valid but empty
|
|
359
|
+
return {}
|
|
360
|
+
|
|
361
|
+
def _get_vcard_inner(self) -> dict[str, Any]:
|
|
362
|
+
"""Extract the inner VCard dict from parsed content.
|
|
363
|
+
|
|
364
|
+
Handles both wrapped ``{"vcard": {...}}`` and flat dict formats.
|
|
365
|
+
Uses ``self._parsed_clm`` inherited from PCard.
|
|
366
|
+
"""
|
|
367
|
+
parsed = self._parsed_clm
|
|
368
|
+
if isinstance(parsed, dict) and "vcard" in parsed:
|
|
369
|
+
return parsed["vcard"]
|
|
370
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
371
|
+
|
|
372
|
+
def _initialize_mutable_state(self, data: dict[str, Any]):
|
|
373
|
+
"""Hydrate mutable lists from parsed content dictionary."""
|
|
374
|
+
# 1. Identity
|
|
375
|
+
self._subject_did = data.get("identity", {}).get("subject_did") or data.get(
|
|
376
|
+
"subject_did", ""
|
|
377
|
+
)
|
|
378
|
+
self._controller_pubkeys = data.get("identity", {}).get(
|
|
379
|
+
"controller_pubkeys"
|
|
380
|
+
) or data.get("controller_pubkeys", [])
|
|
381
|
+
|
|
382
|
+
# 2. Capabilities
|
|
383
|
+
raw_caps = data.get("gatekeeper", {}).get("capabilities") or data.get(
|
|
384
|
+
"capabilities", []
|
|
385
|
+
)
|
|
386
|
+
self._capabilities: list[Capability] = []
|
|
387
|
+
for c in raw_caps:
|
|
388
|
+
try:
|
|
389
|
+
if isinstance(c, dict):
|
|
390
|
+
self._capabilities.append(
|
|
391
|
+
Capability(
|
|
392
|
+
capability_id=c["id"],
|
|
393
|
+
actor_did=c["actor"],
|
|
394
|
+
scope=CapabilityScope(c["scope"]),
|
|
395
|
+
resource_pattern=c["resource_pattern"],
|
|
396
|
+
expires_at=(
|
|
397
|
+
datetime.fromisoformat(c["expires_at"])
|
|
398
|
+
if c.get("expires_at")
|
|
399
|
+
else None
|
|
400
|
+
),
|
|
401
|
+
transferable=c.get("transferable", False),
|
|
402
|
+
constraints=c.get("constraints", {}),
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
except Exception:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
# 3. External Refs (including PCard refs)
|
|
409
|
+
raw_refs = data.get("external_refs", [])
|
|
410
|
+
self._external_refs: list[ExternalRef] = []
|
|
411
|
+
for r in raw_refs:
|
|
412
|
+
if isinstance(r, dict):
|
|
413
|
+
self._external_refs.append(
|
|
414
|
+
ExternalRef(
|
|
415
|
+
uri=r.get("uri", ""),
|
|
416
|
+
content_hash=r.get("content_hash", ""),
|
|
417
|
+
status=r.get("status", "pending"),
|
|
418
|
+
qos_metrics=r.get("qos", {}),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# 4. PCard Refs (Verification) -> Merged into External Refs conceptually or tracked?
|
|
423
|
+
# The docs says PCard refs are in verification block.
|
|
424
|
+
# But old test 'add_pcard_reference' expects them to be retrievable.
|
|
425
|
+
# We will parse them but `add_pcard_reference` typically adds to external refs
|
|
426
|
+
# or we keep a separate list? The doc says "references PCards as procedures".
|
|
427
|
+
# Let's check `get_pcard_references` implementation below.
|
|
428
|
+
|
|
429
|
+
# We will trust `_external_refs` to hold all refs including pcards if they were stored there.
|
|
430
|
+
# But if they are in `verification.pcard_refs`, we should probably track them too.
|
|
431
|
+
self._pcard_refs_hashes = [
|
|
432
|
+
r["hash"] if isinstance(r, dict) else r
|
|
433
|
+
for r in data.get("verification", {}).get("pcard_refs", [])
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
# =========================================================================
|
|
437
|
+
# Role 1: Identity & Credential Container
|
|
438
|
+
# =========================================================================
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def subject_did(self) -> str:
|
|
442
|
+
return self._subject_did
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def controller_pubkeys(self) -> list[str]:
|
|
446
|
+
return self._controller_pubkeys
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def capabilities(self) -> list[Capability]:
|
|
450
|
+
return self._capabilities
|
|
451
|
+
|
|
452
|
+
def add_capability(self, capability: Capability) -> None:
|
|
453
|
+
"""Add a new capability to this VCard (Runtime Memory Only)."""
|
|
454
|
+
self._capabilities.append(capability)
|
|
455
|
+
|
|
456
|
+
def get_valid_capabilities(self) -> list[Capability]:
|
|
457
|
+
"""Get all currently valid capabilities."""
|
|
458
|
+
return [c for c in self._capabilities if c.is_valid()]
|
|
459
|
+
|
|
460
|
+
# =========================================================================
|
|
461
|
+
# Role 2: Verification Hub
|
|
462
|
+
# =========================================================================
|
|
463
|
+
|
|
464
|
+
def add_pcard_reference(self, pcard_hash: str) -> None:
|
|
465
|
+
"""Register a PCard for verification."""
|
|
466
|
+
# Add to the specific list
|
|
467
|
+
if pcard_hash not in self._pcard_refs_hashes:
|
|
468
|
+
self._pcard_refs_hashes.append(pcard_hash)
|
|
469
|
+
|
|
470
|
+
# Also add as generic external ref as per old implementation
|
|
471
|
+
ref = ExternalRef(
|
|
472
|
+
uri=f"pcard://{pcard_hash}", content_hash=pcard_hash, status="verified"
|
|
473
|
+
)
|
|
474
|
+
self.add_external_ref(ref)
|
|
475
|
+
|
|
476
|
+
def get_pcard_references(self) -> list[str]:
|
|
477
|
+
"""Get all registered PCard hashes."""
|
|
478
|
+
# Return unique set of explicitly tracked + those in external refs
|
|
479
|
+
refs = set(self._pcard_refs_hashes)
|
|
480
|
+
for r in self._external_refs:
|
|
481
|
+
if r.uri.startswith("pcard://"):
|
|
482
|
+
refs.add(r.content_hash)
|
|
483
|
+
return list(refs)
|
|
484
|
+
|
|
485
|
+
# =========================================================================
|
|
486
|
+
# Role 3: Side Effect Manager
|
|
487
|
+
# =========================================================================
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def external_refs(self) -> list[ExternalRef]:
|
|
491
|
+
return self._external_refs
|
|
492
|
+
|
|
493
|
+
def add_external_ref(self, ref: ExternalRef) -> None:
|
|
494
|
+
"""Add an external reference (describes a side effect)."""
|
|
495
|
+
self._external_refs.append(ref)
|
|
496
|
+
|
|
497
|
+
def get_external_refs_by_status(self, status: str) -> list[ExternalRef]:
|
|
498
|
+
"""Get external references by verification status."""
|
|
499
|
+
return [r for r in self._external_refs if r.status == status]
|
|
500
|
+
|
|
501
|
+
def verify_external_ref(self, uri: str, new_hash: str) -> bool:
|
|
502
|
+
"""Verify an external reference and update its status."""
|
|
503
|
+
for ref in self._external_refs:
|
|
504
|
+
if ref.uri == uri:
|
|
505
|
+
if ref.content_hash == new_hash:
|
|
506
|
+
ref.status = "verified"
|
|
507
|
+
ref.last_verified = datetime.now()
|
|
508
|
+
return True
|
|
509
|
+
else:
|
|
510
|
+
ref.status = "stale"
|
|
511
|
+
return False
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
def get_managed_event_types(self) -> list[str]:
|
|
515
|
+
"""Get the list of event/MCard types this VCard subscribes to or dispatches."""
|
|
516
|
+
return self._get_vcard_inner().get("managed_event_types", [])
|
|
517
|
+
|
|
518
|
+
# =========================================================================
|
|
519
|
+
# Role 4: Input/Output Gatekeeper
|
|
520
|
+
# =========================================================================
|
|
521
|
+
|
|
522
|
+
def has_capability(self, scope: CapabilityScope, resource_hash: str) -> bool:
|
|
523
|
+
"""Check if VCard has a valid capability for a resource."""
|
|
524
|
+
for cap in self.get_valid_capabilities():
|
|
525
|
+
if cap.scope == scope and cap.matches_resource(resource_hash):
|
|
526
|
+
return True
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
def authorize_ingress(
|
|
530
|
+
self, source_did: str, content_hash: str, capability_id: str | None = None
|
|
531
|
+
) -> bool:
|
|
532
|
+
"""Authorize content entering the PKC (ingress)."""
|
|
533
|
+
authorized = False
|
|
534
|
+
used_capability = None
|
|
535
|
+
|
|
536
|
+
# Check if source has ingress capability
|
|
537
|
+
for cap in self.get_valid_capabilities():
|
|
538
|
+
if cap.actor_did == source_did and cap.scope in [
|
|
539
|
+
CapabilityScope.WRITE,
|
|
540
|
+
CapabilityScope.ADMIN,
|
|
541
|
+
]:
|
|
542
|
+
if capability_id is None or cap.capability_id == capability_id:
|
|
543
|
+
authorized = True
|
|
544
|
+
used_capability = cap.capability_id
|
|
545
|
+
break
|
|
546
|
+
|
|
547
|
+
# Log the gatekeeper event
|
|
548
|
+
event = GatekeeperEvent(
|
|
549
|
+
direction=GatekeeperDirection.INGRESS,
|
|
550
|
+
timestamp=datetime.now(),
|
|
551
|
+
source_did=source_did,
|
|
552
|
+
destination_did=None,
|
|
553
|
+
content_hash=content_hash,
|
|
554
|
+
authorized=authorized,
|
|
555
|
+
capability_used=used_capability,
|
|
556
|
+
)
|
|
557
|
+
self._gatekeeper_log.append(event)
|
|
558
|
+
return authorized
|
|
559
|
+
|
|
560
|
+
def register_for_egress(self, content_hash: str) -> bool:
|
|
561
|
+
"""Register content for potential egress."""
|
|
562
|
+
if content_hash not in self._export_manifest:
|
|
563
|
+
self._export_manifest.append(content_hash)
|
|
564
|
+
return True
|
|
565
|
+
return False
|
|
566
|
+
|
|
567
|
+
def authorize_egress(
|
|
568
|
+
self,
|
|
569
|
+
destination_did: str,
|
|
570
|
+
content_hash: str,
|
|
571
|
+
capability_id: str | None = None,
|
|
572
|
+
) -> bool:
|
|
573
|
+
"""Authorize content leaving the PKC (egress)."""
|
|
574
|
+
if content_hash not in self._export_manifest:
|
|
575
|
+
self._log_egress_attempt(
|
|
576
|
+
destination_did, content_hash, False, "Not registered in manifest"
|
|
577
|
+
)
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
authorized = False
|
|
581
|
+
used_capability = None
|
|
582
|
+
|
|
583
|
+
for cap in self.get_valid_capabilities():
|
|
584
|
+
if cap.scope in [CapabilityScope.READ, CapabilityScope.ADMIN]:
|
|
585
|
+
if cap.matches_resource(content_hash):
|
|
586
|
+
if capability_id is None or cap.capability_id == capability_id:
|
|
587
|
+
authorized = True
|
|
588
|
+
used_capability = cap.capability_id
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
self._log_egress_attempt(
|
|
592
|
+
destination_did, content_hash, authorized, used_capability
|
|
593
|
+
)
|
|
594
|
+
return authorized
|
|
595
|
+
|
|
596
|
+
def _log_egress_attempt(self, dst, hash_val, auth, cap_id):
|
|
597
|
+
event = GatekeeperEvent(
|
|
598
|
+
direction=GatekeeperDirection.EGRESS,
|
|
599
|
+
timestamp=datetime.now(),
|
|
600
|
+
source_did=None,
|
|
601
|
+
destination_did=dst,
|
|
602
|
+
content_hash=hash_val,
|
|
603
|
+
authorized=auth,
|
|
604
|
+
capability_used=cap_id,
|
|
605
|
+
)
|
|
606
|
+
self._gatekeeper_log.append(event)
|
|
607
|
+
|
|
608
|
+
def get_gatekeeper_log(
|
|
609
|
+
self, direction: GatekeeperDirection | None = None
|
|
610
|
+
) -> list[GatekeeperEvent]:
|
|
611
|
+
"""Get gatekeeper audit log."""
|
|
612
|
+
if direction is None:
|
|
613
|
+
return self._gatekeeper_log
|
|
614
|
+
return [e for e in self._gatekeeper_log if e.direction == direction]
|
|
615
|
+
|
|
616
|
+
def get_dots_metadata(self) -> DOTSMetadata:
|
|
617
|
+
"""Get DOTS metadata for this VCard."""
|
|
618
|
+
return create_vcard_dots_metadata(credential_hash=self.hash)
|
|
619
|
+
|
|
620
|
+
# =========================================================================
|
|
621
|
+
# EOS Compliance
|
|
622
|
+
# =========================================================================
|
|
623
|
+
|
|
624
|
+
def simulate_mode(self) -> "VCardSimulation":
|
|
625
|
+
"""Enter simulation mode for EOS compliance."""
|
|
626
|
+
return VCardSimulation(self)
|
|
627
|
+
|
|
628
|
+
# =========================================================================
|
|
629
|
+
# Petri Net Token Semantics
|
|
630
|
+
# =========================================================================
|
|
631
|
+
|
|
632
|
+
def get_token_handle(self) -> str:
|
|
633
|
+
"""Get the handle for this VCard (Token).
|
|
634
|
+
|
|
635
|
+
If verification VCard, returns the creating PCard's balanced handle.
|
|
636
|
+
Otherwise, derives from content.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
Handle string (Place name)
|
|
640
|
+
"""
|
|
641
|
+
v = self._get_vcard_inner()
|
|
642
|
+
|
|
643
|
+
# explicit handle in content takes precedence (e.g. verification output)
|
|
644
|
+
if v.get("handle"):
|
|
645
|
+
return v["handle"]
|
|
646
|
+
if v.get("token_handle"):
|
|
647
|
+
return v["token_handle"]
|
|
648
|
+
|
|
649
|
+
return f"vcard://{self.hash[:16]}"
|
|
650
|
+
|
|
651
|
+
def is_verification_vcard(self) -> bool:
|
|
652
|
+
"""Check if this is a Verification VCard (produced by a PCard execution)."""
|
|
653
|
+
v = self._get_vcard_inner()
|
|
654
|
+
return v.get("type") in ("verification", "verification-result") or bool(
|
|
655
|
+
v.get("verification", {}).get("execution_result")
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
@staticmethod
|
|
659
|
+
def create_verification_vcard(
|
|
660
|
+
pcard: Any,
|
|
661
|
+
execution_result: Any,
|
|
662
|
+
previous_vcard: "VCard | None" = None,
|
|
663
|
+
verified: bool = True,
|
|
664
|
+
hash_function: str = "sha256",
|
|
665
|
+
) -> "VCard":
|
|
666
|
+
"""Create a Verification VCard (Token) from a PCard execution.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
pcard: The PCard (Transition) that executed.
|
|
670
|
+
execution_result: The result of the execution.
|
|
671
|
+
previous_vcard: The input VCard (Token) if any (for provenance).
|
|
672
|
+
verified: Whether execution was successful.
|
|
673
|
+
hash_function: Hash algorithm to use.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
A new VCard instance.
|
|
677
|
+
"""
|
|
678
|
+
# Calculate handle (Place)
|
|
679
|
+
handle = getattr(
|
|
680
|
+
pcard,
|
|
681
|
+
"get_balanced_handle",
|
|
682
|
+
lambda: f"hash/{pcard.hash[:16]}/balanced",
|
|
683
|
+
)()
|
|
684
|
+
|
|
685
|
+
# Construct content
|
|
686
|
+
content = {
|
|
687
|
+
"vcard": {
|
|
688
|
+
"version": "1.0",
|
|
689
|
+
"type": "verification",
|
|
690
|
+
"handle": handle,
|
|
691
|
+
"identity": {"subject_did": "did:ptr:system", "controller_pubkeys": []},
|
|
692
|
+
"verification": {
|
|
693
|
+
"pcard_hash": pcard.hash,
|
|
694
|
+
"execution_result": execution_result,
|
|
695
|
+
"success": verified, # Match JS 'success' field
|
|
696
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
697
|
+
"previous_hash": previous_vcard.hash if previous_vcard else None,
|
|
698
|
+
},
|
|
699
|
+
"gatekeeper": {"capabilities": []},
|
|
700
|
+
"external_refs": [],
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# Create VCard from content
|
|
705
|
+
return VCard(json.dumps(content, sort_keys=True), hash_function=hash_function)
|
|
706
|
+
|
|
707
|
+
def get_source_pcard_hash(self) -> str | None:
|
|
708
|
+
"""Get the hash of the PCard that produced this token."""
|
|
709
|
+
inner = self._get_vcard_inner()
|
|
710
|
+
return inner.get("verification", {}).get("pcard_hash")
|
|
711
|
+
|
|
712
|
+
def get_previous_hash(self) -> str | None:
|
|
713
|
+
"""Get the previous VCard hash in the provenance chain."""
|
|
714
|
+
inner = self._get_vcard_inner()
|
|
715
|
+
return inner.get("verification", {}).get("previous_hash")
|
|
716
|
+
|
|
717
|
+
def enables_transition(self, pcard: "PCard") -> bool:
|
|
718
|
+
"""Check if this VCard can serve as input to the given PCard.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
pcard: The transition to check.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
True if this VCard satisfies one of the inputs.
|
|
725
|
+
"""
|
|
726
|
+
input_refs = pcard.get_input_vcard_refs()
|
|
727
|
+
self.get_token_handle()
|
|
728
|
+
|
|
729
|
+
for ref in input_refs:
|
|
730
|
+
# Check handle match (if we were stored at that handle)
|
|
731
|
+
# Since VCard model doesn't know where it is stored (Collection knows),
|
|
732
|
+
# this check is limited. We check if we match strict hash requirements.
|
|
733
|
+
if ref.get("expected_hash") == self.hash:
|
|
734
|
+
return True
|
|
735
|
+
|
|
736
|
+
return False
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class VCardSimulation:
|
|
740
|
+
"""Simulation context for VCard (EOS Compliance)."""
|
|
741
|
+
|
|
742
|
+
def __init__(self, vcard: VCard):
|
|
743
|
+
self.vcard = vcard
|
|
744
|
+
self.simulation_log: list[dict[str, Any]] = []
|
|
745
|
+
|
|
746
|
+
def log_effect(self, effect_type: str, details: dict[str, Any]) -> None:
|
|
747
|
+
"""Log a simulated side effect."""
|
|
748
|
+
self.simulation_log.append(
|
|
749
|
+
{
|
|
750
|
+
"timestamp": datetime.now().isoformat(),
|
|
751
|
+
"effect_type": effect_type,
|
|
752
|
+
"details": details,
|
|
753
|
+
"simulated": True,
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def get_simulation_log(self) -> list[dict[str, Any]]:
|
|
758
|
+
"""Get the simulation log."""
|
|
759
|
+
return self.simulation_log
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class VCardPre(VCard):
|
|
763
|
+
"""VCardPre acts as a Subscriber/Hook mechanism.
|
|
764
|
+
|
|
765
|
+
It subscribes to particular types of events or MCards inserted into
|
|
766
|
+
the database/execution log, executing hooks to alter its internal state.
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
def fire_hook(self, event_type: str, mcard: MCard) -> None:
|
|
770
|
+
"""Trigger the hook logic for this subscriber."""
|
|
771
|
+
if event_type in self.get_managed_event_types():
|
|
772
|
+
# In a full implementation, this would perform state transitions
|
|
773
|
+
# or record the event. For now, we update the gatekeeper log.
|
|
774
|
+
self.log_gatekeeper_event(
|
|
775
|
+
GatekeeperEvent(
|
|
776
|
+
direction=GatekeeperDirection.INGRESS,
|
|
777
|
+
timestamp=datetime.now(timezone.utc),
|
|
778
|
+
source_did=None,
|
|
779
|
+
destination_did=self.get_subject_did() or "unknown",
|
|
780
|
+
content_hash=mcard.hash,
|
|
781
|
+
authorized=True,
|
|
782
|
+
capability_used="VCardPre_Hook",
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class VCardPost(VCard):
|
|
788
|
+
"""VCardPost acts as a Dispatcher mechanism.
|
|
789
|
+
|
|
790
|
+
It contains a list of items (types) that, if it were to be fired,
|
|
791
|
+
it dispatches or creates relevant types of MCards into the execution log.
|
|
792
|
+
"""
|
|
793
|
+
|
|
794
|
+
def dispatch_events(self, log_db) -> list[MCard]:
|
|
795
|
+
"""Dispatch relevant types of MCards into the execution log."""
|
|
796
|
+
import json
|
|
797
|
+
|
|
798
|
+
from clm.layer0.model.card import MCard
|
|
799
|
+
|
|
800
|
+
dispatched_cards = []
|
|
801
|
+
for event_type in self.get_managed_event_types():
|
|
802
|
+
# Dispatch empty prototype MCards based on the managed types
|
|
803
|
+
# A real implementation would hydrate this from the execution output
|
|
804
|
+
payload = json.dumps({"type": event_type, "dispatched_by": self.hash})
|
|
805
|
+
dispatched = MCard(payload)
|
|
806
|
+
if log_db:
|
|
807
|
+
log_db.add(dispatched)
|
|
808
|
+
dispatched_cards.append(dispatched)
|
|
809
|
+
|
|
810
|
+
# Log the egress
|
|
811
|
+
self.log_gatekeeper_event(
|
|
812
|
+
GatekeeperEvent(
|
|
813
|
+
direction=GatekeeperDirection.EGRESS,
|
|
814
|
+
timestamp=datetime.now(timezone.utc),
|
|
815
|
+
source_did=self.get_subject_did() or "unknown",
|
|
816
|
+
destination_did=None,
|
|
817
|
+
content_hash=dispatched.hash,
|
|
818
|
+
authorized=True,
|
|
819
|
+
capability_used="VCardPost_Dispatch",
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
return dispatched_cards
|