controlloop 0.1.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.
Files changed (107) hide show
  1. controlloop/__init__.py +16 -0
  2. controlloop/__main__.py +6 -0
  3. controlloop/acbom/__init__.py +93 -0
  4. controlloop/acbom/completeness.py +117 -0
  5. controlloop/acbom/identity.py +210 -0
  6. controlloop/acbom/models.py +395 -0
  7. controlloop/acbom/serialization.py +374 -0
  8. controlloop/acbom/versioning.py +104 -0
  9. controlloop/adapters/__init__.py +53 -0
  10. controlloop/adapters/a2a.py +479 -0
  11. controlloop/adapters/crewai.py +646 -0
  12. controlloop/adapters/framework.py +74 -0
  13. controlloop/adapters/framework_versions.py +211 -0
  14. controlloop/adapters/iam.py +485 -0
  15. controlloop/adapters/langgraph.py +749 -0
  16. controlloop/adapters/mcp.py +179 -0
  17. controlloop/adapters/mcp_live.py +582 -0
  18. controlloop/adapters/oasf.py +183 -0
  19. controlloop/adapters/openai_agents.py +824 -0
  20. controlloop/adapters/openapi.py +377 -0
  21. controlloop/adapters/runner.py +116 -0
  22. controlloop/adapters/sandbox.py +66 -0
  23. controlloop/baseline/__init__.py +102 -0
  24. controlloop/baseline/integrity.py +124 -0
  25. controlloop/baseline/merge_base.py +602 -0
  26. controlloop/baseline/models.py +206 -0
  27. controlloop/baseline/provenance.py +162 -0
  28. controlloop/baseline/resolution.py +163 -0
  29. controlloop/baseline/serialization.py +115 -0
  30. controlloop/baseline/staleness.py +227 -0
  31. controlloop/bom.py +226 -0
  32. controlloop/bounded_recursion.py +50 -0
  33. controlloop/capability/__init__.py +48 -0
  34. controlloop/capability/blast_radius.py +289 -0
  35. controlloop/capability/compute.py +479 -0
  36. controlloop/capability/models.py +129 -0
  37. controlloop/capability/taxonomy.py +158 -0
  38. controlloop/cli.py +2368 -0
  39. controlloop/config.py +517 -0
  40. controlloop/diagnostics.py +149 -0
  41. controlloop/diff/__init__.py +87 -0
  42. controlloop/diff/compute.py +298 -0
  43. controlloop/diff/material_summary.py +308 -0
  44. controlloop/diff/models.py +156 -0
  45. controlloop/diff/reduction.py +118 -0
  46. controlloop/diff/report.py +116 -0
  47. controlloop/exit_codes.py +140 -0
  48. controlloop/findings/__init__.py +58 -0
  49. controlloop/findings/identity.py +148 -0
  50. controlloop/findings/models.py +344 -0
  51. controlloop/findings/rendering.py +152 -0
  52. controlloop/ignore.py +178 -0
  53. controlloop/manifest/__init__.py +59 -0
  54. controlloop/manifest/annotation.py +161 -0
  55. controlloop/manifest/approvals.py +92 -0
  56. controlloop/manifest/ceiling.py +441 -0
  57. controlloop/manifest/draft.py +467 -0
  58. controlloop/manifest/models.py +384 -0
  59. controlloop/manifest/parser.py +100 -0
  60. controlloop/manifest/reconciliation.py +148 -0
  61. controlloop/output.py +157 -0
  62. controlloop/policy/__init__.py +126 -0
  63. controlloop/policy/engine.py +279 -0
  64. controlloop/policy/models.py +181 -0
  65. controlloop/policy/rulepack.py +274 -0
  66. controlloop/policy/rules/__init__.py +262 -0
  67. controlloop/policy/rules/_shared.py +86 -0
  68. controlloop/policy/rules/broad_permission.py +270 -0
  69. controlloop/policy/rules/capability_ceiling.py +522 -0
  70. controlloop/policy/rules/destructive_or_financial_action.py +296 -0
  71. controlloop/policy/rules/exfiltration_triangle.py +537 -0
  72. controlloop/policy/rules/hardcoded_secrets.py +194 -0
  73. controlloop/policy/rules/missing_action_logging.py +184 -0
  74. controlloop/policy/rules/sensitive_write_without_approval.py +222 -0
  75. controlloop/policy/rules/unauthorized_delegation.py +241 -0
  76. controlloop/policy/rules/undeclared_tools.py +238 -0
  77. controlloop/policy/rules/unresolved_dynamic_registration.py +199 -0
  78. controlloop/policy/suppressions.py +260 -0
  79. controlloop/policy/thresholds.py +369 -0
  80. controlloop/py.typed +0 -0
  81. controlloop/redaction.py +327 -0
  82. controlloop/reporting/__init__.py +84 -0
  83. controlloop/reporting/cyclonedx.py +116 -0
  84. controlloop/reporting/evidence.py +170 -0
  85. controlloop/reporting/report.py +141 -0
  86. controlloop/reporting/sarif.py +119 -0
  87. controlloop/reporting/terminal.py +286 -0
  88. controlloop/repository.py +786 -0
  89. controlloop/schemas/acbom/1.0.0/acbom.schema.json +472 -0
  90. controlloop/schemas/acbom/1.1.0/acbom.schema.json +481 -0
  91. controlloop/schemas/baseline/1.0.0/baseline.schema.json +243 -0
  92. controlloop/schemas/baseline/1.1.0/baseline.schema.json +423 -0
  93. controlloop/schemas/evidence/1.0.0/evidence.schema.json +805 -0
  94. controlloop/schemas/evidence/1.1.0/evidence.schema.json +833 -0
  95. controlloop/schemas/evidence/1.2.0/evidence.schema.json +886 -0
  96. controlloop/schemas/manifest/1.0.0/manifest.schema.json +408 -0
  97. controlloop/schemas/manifest/1.1.0/manifest.schema.json +568 -0
  98. controlloop/schemas/manifest/1.2.0/manifest.schema.json +568 -0
  99. controlloop/schemas/report/1.0.0/report.schema.json +784 -0
  100. controlloop/schemas/report/1.1.0/report.schema.json +837 -0
  101. controlloop/secret_scan.py +452 -0
  102. controlloop/verbosity.py +201 -0
  103. controlloop-0.1.0.dist-info/METADATA +427 -0
  104. controlloop-0.1.0.dist-info/RECORD +107 -0
  105. controlloop-0.1.0.dist-info/WHEEL +4 -0
  106. controlloop-0.1.0.dist-info/entry_points.txt +2 -0
  107. controlloop-0.1.0.dist-info/licenses/LICENSE +191 -0
@@ -0,0 +1,16 @@
1
+ """ControlLoop Community CLI."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+
6
+ def _distribution_version() -> str:
7
+ """Return the installed version or a safe source-tree fallback."""
8
+ try:
9
+ return version("controlloop")
10
+ except PackageNotFoundError:
11
+ return "0.0.0+uninstalled"
12
+
13
+
14
+ __version__ = _distribution_version()
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Run ControlLoop with ``python -m controlloop``."""
2
+
3
+ from controlloop.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,93 @@
1
+ """Canonical Agent Capability Bill of Materials models."""
2
+
3
+ from controlloop.acbom.completeness import (
4
+ INCOMPLETE_DIAGNOSTIC_REGISTRY,
5
+ completeness_summary,
6
+ incomplete_diagnostic,
7
+ )
8
+ from controlloop.acbom.identity import (
9
+ IDENTITY_VERSION,
10
+ EdgeIdentityMaterial,
11
+ NodeIdentityMaterial,
12
+ SemanticIdentity,
13
+ edge_id,
14
+ node_id,
15
+ )
16
+ from controlloop.acbom.models import (
17
+ ACBOMDocument,
18
+ Confidence,
19
+ Diagnostic,
20
+ DiagnosticSeverity,
21
+ Edge,
22
+ EdgeStatus,
23
+ EdgeType,
24
+ Evidence,
25
+ Generator,
26
+ KnownBoolean,
27
+ Node,
28
+ NodeType,
29
+ Repository,
30
+ SecurityAttributes,
31
+ Sensitivity,
32
+ StandardIdentifier,
33
+ )
34
+ from controlloop.acbom.serialization import (
35
+ CanonicalizationError,
36
+ EdgeMergeConflictError,
37
+ NodeMergeConflictError,
38
+ canonical_json_bytes,
39
+ canonicalize_document,
40
+ compute_digest,
41
+ merge_duplicate_edges,
42
+ merge_duplicate_nodes,
43
+ serialize_document,
44
+ with_digest,
45
+ )
46
+ from controlloop.acbom.versioning import (
47
+ CURRENT_SCHEMA_VERSION,
48
+ IncompatibleSchemaVersionError,
49
+ check_schema_version_compatible,
50
+ load_acbom_document,
51
+ )
52
+
53
+ __all__ = [
54
+ "CURRENT_SCHEMA_VERSION",
55
+ "IDENTITY_VERSION",
56
+ "INCOMPLETE_DIAGNOSTIC_REGISTRY",
57
+ "ACBOMDocument",
58
+ "CanonicalizationError",
59
+ "Confidence",
60
+ "Diagnostic",
61
+ "DiagnosticSeverity",
62
+ "Edge",
63
+ "EdgeIdentityMaterial",
64
+ "EdgeMergeConflictError",
65
+ "EdgeStatus",
66
+ "EdgeType",
67
+ "Evidence",
68
+ "Generator",
69
+ "IncompatibleSchemaVersionError",
70
+ "KnownBoolean",
71
+ "Node",
72
+ "NodeIdentityMaterial",
73
+ "NodeMergeConflictError",
74
+ "NodeType",
75
+ "Repository",
76
+ "SecurityAttributes",
77
+ "SemanticIdentity",
78
+ "Sensitivity",
79
+ "StandardIdentifier",
80
+ "canonical_json_bytes",
81
+ "canonicalize_document",
82
+ "check_schema_version_compatible",
83
+ "completeness_summary",
84
+ "compute_digest",
85
+ "edge_id",
86
+ "incomplete_diagnostic",
87
+ "load_acbom_document",
88
+ "merge_duplicate_edges",
89
+ "merge_duplicate_nodes",
90
+ "node_id",
91
+ "serialize_document",
92
+ "with_digest",
93
+ ]
@@ -0,0 +1,117 @@
1
+ """Unknown and incomplete analysis representation for canonical ACBOMs.
2
+
3
+ Dynamic or unsupported behavior is represented as an explicit unknown --
4
+ never omitted silently, since a silently dropped capability produces a
5
+ confident pass, the worst failure this product can have. This module
6
+ mirrors `controlloop.repository`'s `DIAGNOSTIC_REGISTRY`/`summarize_skips`
7
+ pattern at the canonical ACBOM layer: a closed, documented code registry,
8
+ a constructor enforcing the shape, and a deterministic summary view.
9
+
10
+ No new schema surface is introduced. `Diagnostic.incomplete` and
11
+ `Diagnostic.confidence` already exist on the canonical model; a
12
+ completeness summary is a filtered, sorted view over an
13
+ `ACBOMDocument`'s existing `diagnostics`, not new stored state.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Mapping
19
+
20
+ from controlloop.acbom.models import (
21
+ ACBOMDocument,
22
+ Confidence,
23
+ Diagnostic,
24
+ DiagnosticCode,
25
+ DiagnosticSeverity,
26
+ EvidenceCollection,
27
+ SafeText,
28
+ )
29
+
30
+ _UNRESOLVED_REFERENCE_CODE = "INCOMPLETE-UNRESOLVED-REFERENCE"
31
+ _DYNAMIC_REGISTRATION_CODE = "INCOMPLETE-DYNAMIC-REGISTRATION"
32
+ _UNPARSED_REGION_CODE = "INCOMPLETE-UNPARSED-REGION"
33
+ _BOUND_EXCEEDED_CODE = "INCOMPLETE-BOUND-EXCEEDED"
34
+ _ADAPTER_FAILURE_CODE = "INCOMPLETE-ADAPTER-FAILURE"
35
+ _UNVERIFIED_DECLARATION_CODE = "INCOMPLETE-UNVERIFIED-DECLARATION"
36
+ _CYCLE_DETECTED_CODE = "INCOMPLETE-CYCLE-DETECTED"
37
+
38
+ INCOMPLETE_DIAGNOSTIC_REGISTRY: Mapping[str, str] = {
39
+ _UNRESOLVED_REFERENCE_CODE: (
40
+ "A reference to a capability, tool, or identity could not be "
41
+ "resolved to a concrete node."
42
+ ),
43
+ _DYNAMIC_REGISTRATION_CODE: (
44
+ "A tool, agent, or capability is registered at runtime and "
45
+ "cannot be proven statically."
46
+ ),
47
+ _UNPARSED_REGION_CODE: (
48
+ "A region of an otherwise-supported input could not be parsed."
49
+ ),
50
+ _BOUND_EXCEEDED_CODE: (
51
+ "Analysis stopped because a configured resource or depth bound "
52
+ "was reached before discovery completed."
53
+ ),
54
+ _ADAPTER_FAILURE_CODE: (
55
+ "An adapter raised while detecting or discovering evidence, and "
56
+ "was isolated so the rest of the scan could continue."
57
+ ),
58
+ _UNVERIFIED_DECLARATION_CODE: (
59
+ "A human-authored manifest declaration names a capability that "
60
+ "discovered evidence could not corroborate."
61
+ ),
62
+ _CYCLE_DETECTED_CODE: (
63
+ "A graph traversal reached a node already present earlier on the "
64
+ "same path and stopped there, rather than looping or reporting "
65
+ "an arbitrarily long path through the cycle."
66
+ ),
67
+ }
68
+
69
+
70
+ def incomplete_diagnostic(
71
+ *,
72
+ code: DiagnosticCode,
73
+ message: SafeText,
74
+ evidence: EvidenceCollection,
75
+ ) -> Diagnostic:
76
+ """Return a `Diagnostic` representing an explicit unknown (REQ-1).
77
+
78
+ `code` must be a member of `INCOMPLETE_DIAGNOSTIC_REGISTRY`. Always
79
+ `incomplete=True` and `confidence=Confidence.UNKNOWN` -- an
80
+ incompleteness diagnostic can never claim confident knowledge of the
81
+ very thing it reports as unresolved.
82
+ """
83
+
84
+ if code not in INCOMPLETE_DIAGNOSTIC_REGISTRY:
85
+ raise ValueError(f"{code!r} is not a registered incompleteness code")
86
+
87
+ return Diagnostic(
88
+ code=code,
89
+ severity=DiagnosticSeverity.WARNING,
90
+ message=message,
91
+ confidence=Confidence.UNKNOWN,
92
+ incomplete=True,
93
+ evidence=evidence,
94
+ )
95
+
96
+
97
+ def completeness_summary(document: ACBOMDocument) -> tuple[Diagnostic, ...]:
98
+ """Return every reason `document`'s analysis was incomplete (REQ-2).
99
+
100
+ A deterministic, sorted view over `document.diagnostics` filtered to
101
+ `incomplete=True` -- independent of the input collection's order, the
102
+ same guarantee `repository.summarize_skips` already keeps for
103
+ repository-traversal diagnostics.
104
+ """
105
+
106
+ incomplete = [
107
+ diagnostic for diagnostic in document.diagnostics if diagnostic.incomplete
108
+ ]
109
+ return tuple(
110
+ sorted(
111
+ incomplete,
112
+ key=lambda diagnostic: (
113
+ diagnostic.evidence[0].path or "",
114
+ diagnostic.code,
115
+ ),
116
+ )
117
+ )
@@ -0,0 +1,210 @@
1
+ """Stable, privacy-preserving identifiers for canonical ACBOM components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from collections.abc import Mapping
9
+ from typing import Annotated, Any, ClassVar, Self
10
+
11
+ from pydantic import BaseModel, ConfigDict, StringConstraints, model_validator
12
+
13
+ from controlloop.acbom.models import (
14
+ EdgeId,
15
+ EdgeType,
16
+ NodeId,
17
+ NodeType,
18
+ RelativePath,
19
+ SafeIdentifier,
20
+ )
21
+
22
+ IDENTITY_VERSION = 1
23
+ _EDGE_NAMESPACE = "controlloop.edge"
24
+
25
+ IdentityString = Annotated[
26
+ str,
27
+ StringConstraints(
28
+ strict=True,
29
+ min_length=1,
30
+ max_length=512,
31
+ pattern=r"^[^\x00-\x1f\x7f]*$",
32
+ ),
33
+ ]
34
+
35
+ _PROHIBITED_FIELD_CATEGORIES: tuple[tuple[str, tuple[str, ...]], ...] = (
36
+ ("private key", ("privatekey",)),
37
+ (
38
+ "environment value",
39
+ (
40
+ "environmentvalue",
41
+ "environmentvariablevalue",
42
+ "envvalue",
43
+ "envvarvalue",
44
+ ),
45
+ ),
46
+ ("source content", ("sourcecontent", "sourcecode", "rawsource")),
47
+ ("credential", ("credential",)),
48
+ ("password", ("password", "passwd")),
49
+ ("secret", ("secret",)),
50
+ ("token", ("token",)),
51
+ )
52
+ _EXCLUDED_FIELD_CATEGORIES: tuple[tuple[str, tuple[str, ...]], ...] = (
53
+ ("presentation", ("display", "label", "description")),
54
+ ("evidence", ("evidence", "confidence", "linenumber", "line")),
55
+ ("timestamp", ("timestamp", "createdat", "updatedat")),
56
+ ("version", ("schemaversion", "generatorversion")),
57
+ ("security attribute", ("securityattribute",)),
58
+ )
59
+
60
+
61
+ def _normalized_field_name(field_name: str) -> str:
62
+ return re.sub(r"[^a-z0-9]", "", field_name.casefold())
63
+
64
+
65
+ def _field_category(field_name: object) -> tuple[str, str] | None:
66
+ if not isinstance(field_name, str):
67
+ return ("unsupported", "unsupported")
68
+
69
+ normalized = _normalized_field_name(field_name)
70
+ for category, markers in _PROHIBITED_FIELD_CATEGORIES:
71
+ if any(marker in normalized for marker in markers):
72
+ return ("prohibited", category)
73
+ for category, markers in _EXCLUDED_FIELD_CATEGORIES:
74
+ if any(marker in normalized for marker in markers):
75
+ return ("excluded", category)
76
+ return None
77
+
78
+
79
+ class _IdentityModel(BaseModel):
80
+ model_config = ConfigDict(
81
+ extra="forbid",
82
+ frozen=True,
83
+ hide_input_in_errors=True,
84
+ strict=True,
85
+ )
86
+
87
+ _allowed_fields: ClassVar[frozenset[str]]
88
+
89
+ @model_validator(mode="before")
90
+ @classmethod
91
+ def reject_unapproved_fields(cls, value: Any) -> Any:
92
+ if not isinstance(value, Mapping):
93
+ return value
94
+
95
+ for field_name in value:
96
+ if field_name in cls._allowed_fields:
97
+ continue
98
+ category = _field_category(field_name)
99
+ if category is None:
100
+ raise ValueError("unsupported identity field category")
101
+ disposition, safe_category = category
102
+ raise ValueError(f"{disposition} identity field category: {safe_category}")
103
+ return value
104
+
105
+
106
+ class SemanticIdentity(_IdentityModel):
107
+ """Allowlisted semantic identity for the canonical ACBOM contract."""
108
+
109
+ _allowed_fields = frozenset({"identifier", "source_path", "in_document_key"})
110
+
111
+ identifier: IdentityString | None = None
112
+ source_path: RelativePath | None = None
113
+ in_document_key: IdentityString | None = None
114
+
115
+ @model_validator(mode="after")
116
+ def validate_identity_shape(self) -> Self:
117
+ uses_identifier = self.identifier is not None
118
+ uses_adapter_fallback = (
119
+ self.source_path is not None and self.in_document_key is not None
120
+ )
121
+ if uses_identifier == uses_adapter_fallback:
122
+ raise ValueError(
123
+ "semantic identity requires exactly one approved identity category"
124
+ )
125
+ if (self.source_path is None) != (self.in_document_key is None):
126
+ raise ValueError(
127
+ "adapter fallback identity requires path and in-document key"
128
+ )
129
+ return self
130
+
131
+
132
+ class NodeIdentityMaterial(_IdentityModel):
133
+ """Typed node identity material accepted by the hashing boundary."""
134
+
135
+ _allowed_fields = frozenset({"node_type", "namespace", "semantic_identity"})
136
+
137
+ node_type: NodeType
138
+ namespace: SafeIdentifier
139
+ semantic_identity: SemanticIdentity
140
+
141
+
142
+ class EdgeIdentityMaterial(_IdentityModel):
143
+ """Typed edge identity material accepted by the hashing boundary."""
144
+
145
+ _allowed_fields = frozenset(
146
+ {
147
+ "relationship_type",
148
+ "source_id",
149
+ "target_id",
150
+ "relationship_discriminator",
151
+ }
152
+ )
153
+
154
+ relationship_type: EdgeType
155
+ source_id: NodeId
156
+ target_id: NodeId
157
+ relationship_discriminator: IdentityString
158
+
159
+
160
+ def _canonical_identity_bytes(
161
+ *,
162
+ node_type: str,
163
+ namespace: str,
164
+ semantic_identity: dict[str, object],
165
+ ) -> bytes:
166
+ payload = {
167
+ "identity_version": IDENTITY_VERSION,
168
+ "namespace": namespace,
169
+ "node_type": node_type,
170
+ "semantic_identity": semantic_identity,
171
+ }
172
+ return json.dumps(
173
+ payload,
174
+ ensure_ascii=False,
175
+ allow_nan=False,
176
+ sort_keys=True,
177
+ separators=(",", ":"),
178
+ ).encode("utf-8")
179
+
180
+
181
+ def node_id(material: NodeIdentityMaterial) -> NodeId:
182
+ """Return the stable canonical ID for validated node identity material."""
183
+
184
+ canonical_bytes = _canonical_identity_bytes(
185
+ node_type=material.node_type.value,
186
+ namespace=material.namespace,
187
+ semantic_identity=material.semantic_identity.model_dump(
188
+ mode="json",
189
+ exclude_none=True,
190
+ ),
191
+ )
192
+ digest = hashlib.sha256(canonical_bytes).hexdigest()
193
+ return f"clp:{material.node_type.value}:sha256:{digest}"
194
+
195
+
196
+ def edge_id(material: EdgeIdentityMaterial) -> EdgeId:
197
+ """Return the stable canonical ID for validated edge identity material."""
198
+
199
+ canonical_bytes = _canonical_identity_bytes(
200
+ node_type="edge",
201
+ namespace=_EDGE_NAMESPACE,
202
+ semantic_identity={
203
+ "relationship_discriminator": material.relationship_discriminator,
204
+ "relationship_type": material.relationship_type.value,
205
+ "source_id": material.source_id,
206
+ "target_id": material.target_id,
207
+ },
208
+ )
209
+ digest = hashlib.sha256(canonical_bytes).hexdigest()
210
+ return f"clp:edge:sha256:{digest}"