matrixai-core 1.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.
- matrixai/__init__.py +6 -0
- matrixai/__main__.py +6 -0
- matrixai/actions/__init__.py +105 -0
- matrixai/actions/approval.py +176 -0
- matrixai/actions/composite_integration.py +164 -0
- matrixai/actions/contract.py +131 -0
- matrixai/actions/dryrun.py +187 -0
- matrixai/actions/executor.py +271 -0
- matrixai/actions/parser.py +312 -0
- matrixai/actions/registry.py +98 -0
- matrixai/actions/rollback.py +135 -0
- matrixai/actions/sandbox.py +190 -0
- matrixai/actions/schema.py +90 -0
- matrixai/actions/trace.py +105 -0
- matrixai/agents/__init__.py +62 -0
- matrixai/agents/architect.py +639 -0
- matrixai/agents/auditor.py +64 -0
- matrixai/agents/llm_adapters.py +301 -0
- matrixai/agents/llm_proposal.py +566 -0
- matrixai/agents/mathematical.py +477 -0
- matrixai/agents/optimizer.py +205 -0
- matrixai/agents/planner_verifier.py +227 -0
- matrixai/agents/prompt.py +541 -0
- matrixai/agents/prompt_supervisor.py +310 -0
- matrixai/agents/refinement.py +474 -0
- matrixai/agents/safety.py +12 -0
- matrixai/agents/verifier.py +99 -0
- matrixai/cli.py +3487 -0
- matrixai/compiler/__init__.py +24 -0
- matrixai/compiler/backend_contract.py +1431 -0
- matrixai/compiler/differentiable_python.py +599 -0
- matrixai/compiler/python_backend.py +418 -0
- matrixai/compiler/torch_forward.py +470 -0
- matrixai/continual/__init__.py +108 -0
- matrixai/continual/approval.py +544 -0
- matrixai/continual/collector.py +282 -0
- matrixai/continual/dataset.py +140 -0
- matrixai/continual/drift.py +482 -0
- matrixai/continual/monitor.py +231 -0
- matrixai/continual/parser.py +745 -0
- matrixai/continual/policy_view.py +80 -0
- matrixai/continual/refinement_bridge.py +86 -0
- matrixai/continual/rollback.py +322 -0
- matrixai/continual/trainer.py +615 -0
- matrixai/continual/versioning.py +352 -0
- matrixai/core/__init__.py +62 -0
- matrixai/core/ast_nodes.py +159 -0
- matrixai/core/evaluator.py +202 -0
- matrixai/core/graph.py +115 -0
- matrixai/core/lexer.py +88 -0
- matrixai/core/parser.py +219 -0
- matrixai/core/registry.py +42 -0
- matrixai/core/trace.py +79 -0
- matrixai/errors.py +196 -0
- matrixai/export/__init__.py +45 -0
- matrixai/export/bundle.py +327 -0
- matrixai/export/equivalence.py +334 -0
- matrixai/export/onnx_exporter.py +630 -0
- matrixai/export/wasm_exporter.py +256 -0
- matrixai/forward/__init__.py +48 -0
- matrixai/forward/composite_forward.py +290 -0
- matrixai/forward/composite_torch.py +399 -0
- matrixai/forward/dense_forward.py +99 -0
- matrixai/forward/dense_torch.py +159 -0
- matrixai/functions/__init__.py +117 -0
- matrixai/functions/math_ops.py +70 -0
- matrixai/functions/scoring.py +93 -0
- matrixai/functions/transforms.py +50 -0
- matrixai/ir/__init__.py +100 -0
- matrixai/ir/continual.py +282 -0
- matrixai/ir/expr.py +432 -0
- matrixai/ir/schema.py +437 -0
- matrixai/pack.py +223 -0
- matrixai/parameters/__init__.py +58 -0
- matrixai/parameters/network_params.py +459 -0
- matrixai/parameters/store.py +351 -0
- matrixai/parameters/tensor_bridge.py +242 -0
- matrixai/parser/__init__.py +6 -0
- matrixai/parser/parser.py +1042 -0
- matrixai/playground.py +4765 -0
- matrixai/playground_api.py +100 -0
- matrixai/registry/__init__.py +53 -0
- matrixai/registry/composite_hash.py +99 -0
- matrixai/registry/entry_hash.py +57 -0
- matrixai/registry/layout.py +58 -0
- matrixai/registry/model_registry.py +324 -0
- matrixai/registry/resolver.py +85 -0
- matrixai/registry/schema.py +82 -0
- matrixai/registry/signing.py +93 -0
- matrixai/runtime/__init__.py +6 -0
- matrixai/runtime/runtime.py +429 -0
- matrixai/sandbox.py +134 -0
- matrixai/scaffolding.py +102 -0
- matrixai/server.py +1490 -0
- matrixai/signing/__init__.py +6 -0
- matrixai/signing/keystore.py +229 -0
- matrixai/templates/classification/README.md +43 -0
- matrixai/templates/classification/dataset/test.csv +6 -0
- matrixai/templates/classification/dataset/train.csv +16 -0
- matrixai/templates/classification/input/sample.json +5 -0
- matrixai/templates/classification/models/best/params.json +22 -0
- matrixai/templates/classification/{{project_name}}.mxai +23 -0
- matrixai/templates/classification/{{project_name}}.mxtrain +32 -0
- matrixai/tooling.py +501 -0
- matrixai/training/__init__.py +162 -0
- matrixai/training/categorical.py +256 -0
- matrixai/training/composite_backprop.py +404 -0
- matrixai/training/composite_evaluator.py +132 -0
- matrixai/training/composite_generator.py +297 -0
- matrixai/training/composite_trainer.py +259 -0
- matrixai/training/data.py +281 -0
- matrixai/training/dataset_manifest.py +523 -0
- matrixai/training/dense_backprop.py +213 -0
- matrixai/training/dense_evaluator.py +265 -0
- matrixai/training/dense_generator.py +459 -0
- matrixai/training/dense_trainer.py +328 -0
- matrixai/training/differentiability.py +166 -0
- matrixai/training/generator.py +394 -0
- matrixai/training/parser.py +396 -0
- matrixai/training/spec.py +267 -0
- matrixai/training/supervised_prompt.py +406 -0
- matrixai/training/synthetic.py +322 -0
- matrixai/training/torch_evaluator.py +173 -0
- matrixai/training/torch_trainer.py +290 -0
- matrixai/training/trainer.py +1294 -0
- matrixai/training/verifier.py +346 -0
- matrixai/types.py +1174 -0
- matrixai_core-1.0.0.dist-info/METADATA +936 -0
- matrixai_core-1.0.0.dist-info/RECORD +133 -0
- matrixai_core-1.0.0.dist-info/WHEEL +5 -0
- matrixai_core-1.0.0.dist-info/entry_points.txt +2 -0
- matrixai_core-1.0.0.dist-info/licenses/LICENSE +661 -0
- matrixai_core-1.0.0.dist-info/top_level.txt +1 -0
matrixai/__init__.py
ADDED
matrixai/__main__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Roberto Llamosas Conde
|
|
3
|
+
|
|
4
|
+
from matrixai.actions.schema import (
|
|
5
|
+
ActionContractSpec,
|
|
6
|
+
RollbackSpec,
|
|
7
|
+
RateLimitSpec,
|
|
8
|
+
SandboxLimitsSpec,
|
|
9
|
+
CAPABILITIES,
|
|
10
|
+
HIGH_RISK_CAPABILITIES,
|
|
11
|
+
MUTATING_CAPABILITIES,
|
|
12
|
+
)
|
|
13
|
+
from matrixai.actions.parser import MxactParseError, parse_mxact
|
|
14
|
+
from matrixai.actions.registry import (
|
|
15
|
+
CapabilityRegistry,
|
|
16
|
+
REQUIRED_SCOPE_FIELDS,
|
|
17
|
+
ScopeValidationResult,
|
|
18
|
+
registry,
|
|
19
|
+
)
|
|
20
|
+
from matrixai.actions.dryrun import (
|
|
21
|
+
DryRunReport,
|
|
22
|
+
DryRunSimulator,
|
|
23
|
+
RateTracker,
|
|
24
|
+
)
|
|
25
|
+
from matrixai.actions.executor import (
|
|
26
|
+
ActionExecutor,
|
|
27
|
+
ActionExecutorError,
|
|
28
|
+
ActionResult,
|
|
29
|
+
ExecutionContext,
|
|
30
|
+
)
|
|
31
|
+
from matrixai.actions.sandbox import (
|
|
32
|
+
SandboxedActionExecutor,
|
|
33
|
+
SandboxedExecutorError,
|
|
34
|
+
SandboxParams,
|
|
35
|
+
SandboxResult,
|
|
36
|
+
)
|
|
37
|
+
from matrixai.actions.trace import (
|
|
38
|
+
ActionTrace,
|
|
39
|
+
build_action_trace,
|
|
40
|
+
sign_action_trace,
|
|
41
|
+
verify_action_trace,
|
|
42
|
+
)
|
|
43
|
+
from matrixai.actions.rollback import (
|
|
44
|
+
RollbackManager,
|
|
45
|
+
RollbackResult,
|
|
46
|
+
RollbackError,
|
|
47
|
+
)
|
|
48
|
+
from matrixai.actions.approval import (
|
|
49
|
+
ApprovalStore,
|
|
50
|
+
ApprovalError,
|
|
51
|
+
HumanApprovalGate,
|
|
52
|
+
PendingExecution,
|
|
53
|
+
)
|
|
54
|
+
from matrixai.actions.contract import (
|
|
55
|
+
ActionContractValidationResult,
|
|
56
|
+
canonical_dict,
|
|
57
|
+
check_signing_key_available,
|
|
58
|
+
compute_action_contract_hash,
|
|
59
|
+
require_signing_key,
|
|
60
|
+
validate_action_contract,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"ActionContractSpec",
|
|
65
|
+
"ActionContractValidationResult",
|
|
66
|
+
"MxactParseError",
|
|
67
|
+
"RollbackSpec",
|
|
68
|
+
"RateLimitSpec",
|
|
69
|
+
"SandboxLimitsSpec",
|
|
70
|
+
"CAPABILITIES",
|
|
71
|
+
"HIGH_RISK_CAPABILITIES",
|
|
72
|
+
"MUTATING_CAPABILITIES",
|
|
73
|
+
"canonical_dict",
|
|
74
|
+
"check_signing_key_available",
|
|
75
|
+
"compute_action_contract_hash",
|
|
76
|
+
"parse_mxact",
|
|
77
|
+
"require_signing_key",
|
|
78
|
+
"validate_action_contract",
|
|
79
|
+
"CapabilityRegistry",
|
|
80
|
+
"REQUIRED_SCOPE_FIELDS",
|
|
81
|
+
"ScopeValidationResult",
|
|
82
|
+
"registry",
|
|
83
|
+
"DryRunReport",
|
|
84
|
+
"DryRunSimulator",
|
|
85
|
+
"RateTracker",
|
|
86
|
+
"ActionExecutor",
|
|
87
|
+
"ActionExecutorError",
|
|
88
|
+
"ActionResult",
|
|
89
|
+
"ExecutionContext",
|
|
90
|
+
"SandboxedActionExecutor",
|
|
91
|
+
"SandboxedExecutorError",
|
|
92
|
+
"SandboxParams",
|
|
93
|
+
"SandboxResult",
|
|
94
|
+
"ActionTrace",
|
|
95
|
+
"build_action_trace",
|
|
96
|
+
"sign_action_trace",
|
|
97
|
+
"verify_action_trace",
|
|
98
|
+
"RollbackManager",
|
|
99
|
+
"RollbackResult",
|
|
100
|
+
"RollbackError",
|
|
101
|
+
"ApprovalStore",
|
|
102
|
+
"ApprovalError",
|
|
103
|
+
"HumanApprovalGate",
|
|
104
|
+
"PendingExecution",
|
|
105
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Roberto Llamosas Conde
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from matrixai.actions.dryrun import _input_hash as _hash_input
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from matrixai.actions.executor import ExecutionContext
|
|
15
|
+
|
|
16
|
+
_DEFAULT_TTL_SECONDS = 300 # 5 minutes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PendingExecution:
|
|
21
|
+
execution_id: str
|
|
22
|
+
contract_name: str
|
|
23
|
+
capability: str
|
|
24
|
+
input_data: dict
|
|
25
|
+
input_hash: str # hash of input_data at submit time
|
|
26
|
+
model_hash: str
|
|
27
|
+
parameter_set_id: str
|
|
28
|
+
action_contract_hash: str
|
|
29
|
+
created_at: str # ISO8601 UTC
|
|
30
|
+
expires_at: str # ISO8601 UTC
|
|
31
|
+
status: str = "pending" # pending | approved | rejected | expired
|
|
32
|
+
channel: str | None = None
|
|
33
|
+
decided_at: str | None = None
|
|
34
|
+
decided_by: str | None = None
|
|
35
|
+
|
|
36
|
+
def is_expired(self, now: datetime | None = None) -> bool:
|
|
37
|
+
ts = now or datetime.now(tz=timezone.utc)
|
|
38
|
+
return ts >= datetime.fromisoformat(self.expires_at)
|
|
39
|
+
|
|
40
|
+
def is_approved(self, now: datetime | None = None) -> bool:
|
|
41
|
+
return self.status == "approved" and not self.is_expired(now)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ApprovalStore:
|
|
45
|
+
"""In-memory store for PendingExecution records."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._store: dict[str, PendingExecution] = {}
|
|
49
|
+
|
|
50
|
+
def submit(
|
|
51
|
+
self,
|
|
52
|
+
context: "ExecutionContext",
|
|
53
|
+
*,
|
|
54
|
+
ttl_seconds: int = _DEFAULT_TTL_SECONDS,
|
|
55
|
+
channel: str | None = None,
|
|
56
|
+
now: datetime | None = None,
|
|
57
|
+
) -> PendingExecution:
|
|
58
|
+
ts = now or datetime.now(tz=timezone.utc)
|
|
59
|
+
execution_id = str(uuid.uuid4())
|
|
60
|
+
pending = PendingExecution(
|
|
61
|
+
execution_id=execution_id,
|
|
62
|
+
contract_name=context.contract.name,
|
|
63
|
+
capability=context.contract.capability,
|
|
64
|
+
input_data=context.input_data,
|
|
65
|
+
input_hash=context.dry_run_report.input_hash,
|
|
66
|
+
model_hash=context.model_hash,
|
|
67
|
+
parameter_set_id=context.parameter_set_id,
|
|
68
|
+
action_contract_hash=context.dry_run_report.action_contract_hash,
|
|
69
|
+
created_at=ts.isoformat(),
|
|
70
|
+
expires_at=(ts + timedelta(seconds=ttl_seconds)).isoformat(),
|
|
71
|
+
status="pending",
|
|
72
|
+
channel=channel,
|
|
73
|
+
)
|
|
74
|
+
self._store[execution_id] = pending
|
|
75
|
+
return pending
|
|
76
|
+
|
|
77
|
+
def get(self, execution_id: str) -> PendingExecution | None:
|
|
78
|
+
return self._store.get(execution_id)
|
|
79
|
+
|
|
80
|
+
def approve(
|
|
81
|
+
self,
|
|
82
|
+
execution_id: str,
|
|
83
|
+
*,
|
|
84
|
+
decided_by: str | None = None,
|
|
85
|
+
now: datetime | None = None,
|
|
86
|
+
) -> PendingExecution:
|
|
87
|
+
pending = self._get_or_raise(execution_id)
|
|
88
|
+
self._check_not_decided(pending)
|
|
89
|
+
if pending.is_expired(now):
|
|
90
|
+
pending.status = "expired"
|
|
91
|
+
raise ApprovalError(f"Execution {execution_id!r} has expired")
|
|
92
|
+
ts = now or datetime.now(tz=timezone.utc)
|
|
93
|
+
pending.status = "approved"
|
|
94
|
+
pending.decided_at = ts.isoformat()
|
|
95
|
+
pending.decided_by = decided_by
|
|
96
|
+
return pending
|
|
97
|
+
|
|
98
|
+
def reject(
|
|
99
|
+
self,
|
|
100
|
+
execution_id: str,
|
|
101
|
+
*,
|
|
102
|
+
decided_by: str | None = None,
|
|
103
|
+
now: datetime | None = None,
|
|
104
|
+
) -> PendingExecution:
|
|
105
|
+
pending = self._get_or_raise(execution_id)
|
|
106
|
+
self._check_not_decided(pending)
|
|
107
|
+
ts = now or datetime.now(tz=timezone.utc)
|
|
108
|
+
pending.status = "rejected"
|
|
109
|
+
pending.decided_at = ts.isoformat()
|
|
110
|
+
pending.decided_by = decided_by
|
|
111
|
+
return pending
|
|
112
|
+
|
|
113
|
+
def list_pending(self, now: datetime | None = None) -> list[PendingExecution]:
|
|
114
|
+
ts = now or datetime.now(tz=timezone.utc)
|
|
115
|
+
result = []
|
|
116
|
+
for p in self._store.values():
|
|
117
|
+
if p.status == "pending":
|
|
118
|
+
if p.is_expired(ts):
|
|
119
|
+
p.status = "expired"
|
|
120
|
+
else:
|
|
121
|
+
result.append(p)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
def _get_or_raise(self, execution_id: str) -> PendingExecution:
|
|
125
|
+
p = self._store.get(execution_id)
|
|
126
|
+
if p is None:
|
|
127
|
+
raise ApprovalError(f"No pending execution found with id {execution_id!r}")
|
|
128
|
+
return p
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _check_not_decided(pending: PendingExecution) -> None:
|
|
132
|
+
if pending.status in ("approved", "rejected"):
|
|
133
|
+
raise ApprovalError(
|
|
134
|
+
f"Execution {pending.execution_id!r} is already {pending.status}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ApprovalError(Exception):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class HumanApprovalGate:
|
|
143
|
+
"""Check whether a context requires human approval and whether it has been granted."""
|
|
144
|
+
|
|
145
|
+
def __init__(self, store: ApprovalStore) -> None:
|
|
146
|
+
self._store = store
|
|
147
|
+
|
|
148
|
+
def requires_approval(self, context: "ExecutionContext") -> bool:
|
|
149
|
+
return context.contract.human_approval
|
|
150
|
+
|
|
151
|
+
def find_approved(
|
|
152
|
+
self,
|
|
153
|
+
context: "ExecutionContext",
|
|
154
|
+
now: datetime | None = None,
|
|
155
|
+
) -> PendingExecution | None:
|
|
156
|
+
"""Return the first approved, non-expired PendingExecution matching this context."""
|
|
157
|
+
for pending in self._store._store.values():
|
|
158
|
+
if (
|
|
159
|
+
pending.action_contract_hash == context.dry_run_report.action_contract_hash
|
|
160
|
+
and pending.input_hash == context.dry_run_report.input_hash
|
|
161
|
+
and pending.model_hash == context.model_hash
|
|
162
|
+
and pending.parameter_set_id == context.parameter_set_id
|
|
163
|
+
and pending.is_approved(now)
|
|
164
|
+
):
|
|
165
|
+
return pending
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def check(
|
|
169
|
+
self,
|
|
170
|
+
context: "ExecutionContext",
|
|
171
|
+
now: datetime | None = None,
|
|
172
|
+
) -> bool:
|
|
173
|
+
"""Return True if approval is not required, or if a valid approval exists."""
|
|
174
|
+
if not self.requires_approval(context):
|
|
175
|
+
return True
|
|
176
|
+
return self.find_approved(context, now) is not None
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Roberto Llamosas Conde
|
|
3
|
+
|
|
4
|
+
"""P21 C10 — P20 action contract integration with composite models."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ComponentChainEntry:
|
|
13
|
+
alias: str
|
|
14
|
+
registry_name: str
|
|
15
|
+
version: str
|
|
16
|
+
mode: str
|
|
17
|
+
entry_hash: str
|
|
18
|
+
interpretability_level: str
|
|
19
|
+
is_terminal: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class CompositeAuditResult:
|
|
24
|
+
composite_model_hash: str
|
|
25
|
+
component_chain: list[ComponentChainEntry]
|
|
26
|
+
errors: list[str]
|
|
27
|
+
warnings: list[str]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def ok(self) -> bool:
|
|
31
|
+
return not self.errors
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_component_chain(program: Any, registry: Any) -> list[ComponentChainEntry]:
|
|
35
|
+
"""Return ordered list of imported components with registry info."""
|
|
36
|
+
from matrixai.registry.model_registry import EntryNotFoundError
|
|
37
|
+
|
|
38
|
+
# Find terminal nodes: nodes that have no outgoing edges to other import nodes
|
|
39
|
+
import_aliases = {imp.alias for imp in getattr(program, "imports", [])}
|
|
40
|
+
out_nodes = {src for src, dst in program.graph.edges}
|
|
41
|
+
terminal_aliases = import_aliases - (import_aliases & out_nodes)
|
|
42
|
+
|
|
43
|
+
chain = []
|
|
44
|
+
for imp in sorted(getattr(program, "imports", []), key=lambda x: x.alias):
|
|
45
|
+
try:
|
|
46
|
+
entry = registry.get(imp.registry_name, imp.version)
|
|
47
|
+
chain.append(ComponentChainEntry(
|
|
48
|
+
alias=imp.alias,
|
|
49
|
+
registry_name=imp.registry_name,
|
|
50
|
+
version=imp.version,
|
|
51
|
+
mode=imp.mode,
|
|
52
|
+
entry_hash=entry.entry_hash,
|
|
53
|
+
interpretability_level=entry.interpretability_level,
|
|
54
|
+
is_terminal=imp.alias in terminal_aliases,
|
|
55
|
+
))
|
|
56
|
+
except EntryNotFoundError:
|
|
57
|
+
chain.append(ComponentChainEntry(
|
|
58
|
+
alias=imp.alias,
|
|
59
|
+
registry_name=imp.registry_name,
|
|
60
|
+
version=imp.version,
|
|
61
|
+
mode=imp.mode,
|
|
62
|
+
entry_hash="",
|
|
63
|
+
interpretability_level="unknown",
|
|
64
|
+
is_terminal=imp.alias in terminal_aliases,
|
|
65
|
+
))
|
|
66
|
+
return chain
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_composite_action_contract(
|
|
70
|
+
program: Any,
|
|
71
|
+
registry: Any,
|
|
72
|
+
*,
|
|
73
|
+
contract: Any = None,
|
|
74
|
+
) -> CompositeAuditResult:
|
|
75
|
+
"""Validate that the composite program satisfies action contract rules.
|
|
76
|
+
|
|
77
|
+
Rules:
|
|
78
|
+
- Intermediate imported components must not have real_with_audit actions (blockers).
|
|
79
|
+
- Only the terminal node may carry an action contract.
|
|
80
|
+
"""
|
|
81
|
+
from matrixai.registry.composite_hash import compute_composite_model_hash
|
|
82
|
+
from matrixai.registry.model_registry import EntryNotFoundError
|
|
83
|
+
|
|
84
|
+
errors: list[str] = []
|
|
85
|
+
warnings: list[str] = []
|
|
86
|
+
|
|
87
|
+
import_aliases = {imp.alias for imp in getattr(program, "imports", [])}
|
|
88
|
+
# A node is intermediate if it has both incoming AND outgoing edges to/from import nodes
|
|
89
|
+
in_nodes = {dst for src, dst in program.graph.edges}
|
|
90
|
+
out_nodes = {src for src, dst in program.graph.edges}
|
|
91
|
+
intermediate_aliases = import_aliases & in_nodes & out_nodes
|
|
92
|
+
|
|
93
|
+
for imp in getattr(program, "imports", []):
|
|
94
|
+
if imp.alias not in intermediate_aliases:
|
|
95
|
+
continue
|
|
96
|
+
try:
|
|
97
|
+
entry = registry.get(imp.registry_name, imp.version)
|
|
98
|
+
blockers = getattr(entry, "blockers", [])
|
|
99
|
+
if "real_with_audit_action" in blockers:
|
|
100
|
+
errors.append(
|
|
101
|
+
f"Intermediate component {imp.alias!r} has real_with_audit action — "
|
|
102
|
+
f"only the terminal component may carry real actions"
|
|
103
|
+
)
|
|
104
|
+
except EntryNotFoundError:
|
|
105
|
+
errors.append(f"Component {imp.alias!r} not found in registry")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
composite_hash = compute_composite_model_hash(program, registry)
|
|
109
|
+
except Exception as exc: # noqa: BLE001
|
|
110
|
+
errors.append(f"Cannot compute composite_model_hash: {exc}")
|
|
111
|
+
composite_hash = ""
|
|
112
|
+
|
|
113
|
+
chain = get_component_chain(program, registry)
|
|
114
|
+
return CompositeAuditResult(
|
|
115
|
+
composite_model_hash=composite_hash,
|
|
116
|
+
component_chain=chain,
|
|
117
|
+
errors=errors,
|
|
118
|
+
warnings=warnings,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def composite_dry_run(
|
|
123
|
+
program: Any,
|
|
124
|
+
registry: Any,
|
|
125
|
+
input_data: Any,
|
|
126
|
+
*,
|
|
127
|
+
parameter_set: Any = None,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
"""Resolve all imports and simulate forward pass for dry-run validation."""
|
|
130
|
+
from matrixai.registry.resolver import CompositeModelResolver, ImportResolutionError
|
|
131
|
+
from matrixai.registry.composite_hash import compute_composite_model_hash
|
|
132
|
+
|
|
133
|
+
errors: list[str] = []
|
|
134
|
+
|
|
135
|
+
# Resolve all imports
|
|
136
|
+
resolver = CompositeModelResolver(registry)
|
|
137
|
+
try:
|
|
138
|
+
hydrated_imports = resolver.resolve(program, allow_mutable_tags=True)
|
|
139
|
+
except ImportResolutionError as exc:
|
|
140
|
+
errors.append(str(exc))
|
|
141
|
+
hydrated_imports = []
|
|
142
|
+
|
|
143
|
+
composite_hash = ""
|
|
144
|
+
if not errors:
|
|
145
|
+
try:
|
|
146
|
+
composite_hash = compute_composite_model_hash(program, registry)
|
|
147
|
+
except Exception as exc: # noqa: BLE001
|
|
148
|
+
errors.append(str(exc))
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"ok": not errors,
|
|
152
|
+
"errors": errors,
|
|
153
|
+
"resolved_imports": [
|
|
154
|
+
{
|
|
155
|
+
"alias": imp.alias,
|
|
156
|
+
"registry_name": imp.registry_name,
|
|
157
|
+
"version": imp.version,
|
|
158
|
+
"resolved_entry_hash": imp.resolved_entry_hash,
|
|
159
|
+
}
|
|
160
|
+
for imp in hydrated_imports
|
|
161
|
+
],
|
|
162
|
+
"composite_model_hash": composite_hash,
|
|
163
|
+
"input": input_data,
|
|
164
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Roberto Llamosas Conde
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from matrixai.actions.registry import registry
|
|
12
|
+
from matrixai.actions.schema import (
|
|
13
|
+
ActionContractSpec,
|
|
14
|
+
CAPABILITIES,
|
|
15
|
+
RateLimitSpec,
|
|
16
|
+
RollbackSpec,
|
|
17
|
+
SandboxLimitsSpec,
|
|
18
|
+
)
|
|
19
|
+
from matrixai.ir.schema import MatrixAIProgram
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── canonical serialization ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
def _ser_rollback(rb: RollbackSpec | None) -> dict | None:
|
|
25
|
+
if rb is None:
|
|
26
|
+
return None
|
|
27
|
+
return {
|
|
28
|
+
"capability": rb.capability,
|
|
29
|
+
"name": rb.name,
|
|
30
|
+
"scope": dict(sorted(rb.scope.items())),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ser_rate_limit(rl: RateLimitSpec | None) -> dict | None:
|
|
35
|
+
if rl is None:
|
|
36
|
+
return None
|
|
37
|
+
return {"per_hour": rl.per_hour, "per_minute": rl.per_minute}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ser_sandbox_limits(sl: SandboxLimitsSpec | None) -> dict | None:
|
|
41
|
+
if sl is None:
|
|
42
|
+
return None
|
|
43
|
+
return {
|
|
44
|
+
"allowed_env_vars": sorted(sl.allowed_env_vars),
|
|
45
|
+
"max_cpu_seconds": sl.max_cpu_seconds,
|
|
46
|
+
"max_memory_mb": sl.max_memory_mb,
|
|
47
|
+
"max_wall_seconds": sl.max_wall_seconds,
|
|
48
|
+
"no_network": sl.no_network,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def canonical_dict(spec: ActionContractSpec) -> dict:
|
|
53
|
+
"""Canonical dict for deterministic hashing — keys always sorted."""
|
|
54
|
+
return {
|
|
55
|
+
"approval_channel": spec.approval_channel,
|
|
56
|
+
"approval_timeout_seconds": spec.approval_timeout_seconds,
|
|
57
|
+
"capability": spec.capability,
|
|
58
|
+
"dry_run_required": spec.dry_run_required,
|
|
59
|
+
"human_approval": spec.human_approval,
|
|
60
|
+
"name": spec.name,
|
|
61
|
+
"rate_limit": _ser_rate_limit(spec.rate_limit),
|
|
62
|
+
"risk_level": CAPABILITIES[spec.capability],
|
|
63
|
+
"rollback": _ser_rollback(spec.rollback),
|
|
64
|
+
"sandbox_limits": _ser_sandbox_limits(spec.sandbox_limits),
|
|
65
|
+
"sandbox_required": spec.sandbox_required,
|
|
66
|
+
"scope": dict(sorted(spec.scope.items())),
|
|
67
|
+
"signature_required": spec.signature_required,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compute_action_contract_hash(spec: ActionContractSpec) -> str:
|
|
72
|
+
"""Return 'sha256:<hex>' of the canonical JSON of spec."""
|
|
73
|
+
payload = json.dumps(canonical_dict(spec), sort_keys=True, separators=(",", ":"))
|
|
74
|
+
digest = hashlib.sha256(payload.encode()).hexdigest()
|
|
75
|
+
return f"sha256:{digest}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── compatibility validation ──────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ActionContractValidationResult:
|
|
82
|
+
ok: bool
|
|
83
|
+
errors: list[str]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def validate_action_contract(
|
|
87
|
+
spec: ActionContractSpec,
|
|
88
|
+
program: MatrixAIProgram,
|
|
89
|
+
) -> ActionContractValidationResult:
|
|
90
|
+
"""Validate that spec is compatible with the .mxai program."""
|
|
91
|
+
errors: list[str] = []
|
|
92
|
+
|
|
93
|
+
matching = [a for a in program.actions if a.name == spec.name]
|
|
94
|
+
if not matching:
|
|
95
|
+
errors.append(f"Action {spec.name!r} not found in program actions")
|
|
96
|
+
return ActionContractValidationResult(ok=False, errors=errors)
|
|
97
|
+
|
|
98
|
+
action = matching[0]
|
|
99
|
+
|
|
100
|
+
if action.target and action.target != spec.capability:
|
|
101
|
+
errors.append(
|
|
102
|
+
f"Action {spec.name!r} TARGET {action.target!r} does not match "
|
|
103
|
+
f"contract CAPABILITY {spec.capability!r}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if action.policy != "real_with_audit":
|
|
107
|
+
errors.append(
|
|
108
|
+
f"Action {spec.name!r} must have POLICY real_with_audit, got {action.policy!r}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
scope_result = registry.validate_scope(spec.capability, spec.scope)
|
|
112
|
+
if not scope_result.ok:
|
|
113
|
+
errors.extend(scope_result.errors)
|
|
114
|
+
|
|
115
|
+
return ActionContractValidationResult(ok=len(errors) == 0, errors=errors)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── signing key check ─────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def check_signing_key_available() -> bool:
|
|
121
|
+
"""Return True if MATRIXAI_ACTION_SIGNING_KEY is set in environment."""
|
|
122
|
+
return bool(os.environ.get("MATRIXAI_ACTION_SIGNING_KEY", ""))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def require_signing_key(spec: ActionContractSpec) -> None:
|
|
126
|
+
"""Raise RuntimeError if spec requires a signing key that is not available."""
|
|
127
|
+
if spec.signature_required and not check_signing_key_available():
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
f"ActionContract {spec.name!r} requires MATRIXAI_ACTION_SIGNING_KEY "
|
|
130
|
+
f"but it is not set in the environment"
|
|
131
|
+
)
|