agentevo-core 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.
@@ -0,0 +1,181 @@
1
+ """Agentevo core domain contracts (classification, events, objects, trust, errors)."""
2
+
3
+ from agentevo_core.actions import (
4
+ MAX_ACTION_STRING_LENGTH as MAX_ACTION_STRING_LENGTH,
5
+ MAX_ACTION_TYPE_LENGTH as MAX_ACTION_TYPE_LENGTH,
6
+ MAX_TIMEOUT_SECONDS as MAX_TIMEOUT_SECONDS,
7
+ ActionFallback as ActionFallback,
8
+ ActionIdempotency as ActionIdempotency,
9
+ ActionTarget as ActionTarget,
10
+ TargetKind as TargetKind,
11
+ TypedAction as TypedAction,
12
+ )
13
+ from agentevo_core.classification import (
14
+ Classification as Classification,
15
+ PrivacyMode as PrivacyMode,
16
+ inherit_source as inherit_source,
17
+ must_redact_before_persistence as must_redact_before_persistence,
18
+ saas_egress_allowed as saas_egress_allowed,
19
+ )
20
+ from agentevo_core.compat import Compat as Compat, auto_exec_allowed as auto_exec_allowed
21
+ from agentevo_core.dlp_patterns import (
22
+ DETECTOR_PATTERNS as DETECTOR_PATTERNS,
23
+ DetectorPattern as DetectorPattern,
24
+ )
25
+ from agentevo_core.errors import (
26
+ HTTP_STATUS as HTTP_STATUS,
27
+ AgentevoError as AgentevoError,
28
+ ErrorCode as ErrorCode,
29
+ retryable as retryable,
30
+ )
31
+ from agentevo_core.events import (
32
+ CanonicalEvent as CanonicalEvent,
33
+ EventEnvironment as EventEnvironment,
34
+ EventProvenance as EventProvenance,
35
+ EventSource as EventSource,
36
+ EventType as EventType,
37
+ compute_content_hash as compute_content_hash,
38
+ )
39
+ from agentevo_core.idempotency import (
40
+ MAX_IDEMPOTENCY_KEY_LENGTH as MAX_IDEMPOTENCY_KEY_LENGTH,
41
+ IdempotencyTier as IdempotencyTier,
42
+ auto_retry_allowed as auto_retry_allowed,
43
+ body_conflicts as body_conflicts,
44
+ check_expected_version as check_expected_version,
45
+ validate_key as validate_key,
46
+ )
47
+ from agentevo_core.metering import (
48
+ BILLABLE_OPERATIONS as BILLABLE_OPERATIONS,
49
+ FREE_MONTHLY_BILLABLE_REQUESTS as FREE_MONTHLY_BILLABLE_REQUESTS,
50
+ MAX_BATCH_EVENTS as MAX_BATCH_EVENTS,
51
+ NON_BILLABLE_OPERATIONS as NON_BILLABLE_OPERATIONS,
52
+ is_billable as is_billable,
53
+ )
54
+ from agentevo_core.objects import (
55
+ LearnedObject as LearnedObject,
56
+ LearnedObjectType as LearnedObjectType,
57
+ Ownership as Ownership,
58
+ Validity as Validity,
59
+ Verification as Verification,
60
+ )
61
+ from agentevo_core.risk import (
62
+ GuardVerdict as GuardVerdict,
63
+ RiskClass as RiskClass,
64
+ apply_policy_risk as apply_policy_risk,
65
+ is_high_or_destructive as is_high_or_destructive,
66
+ requires_human_approval as requires_human_approval,
67
+ )
68
+ from agentevo_core.sync import (
69
+ CURSOR_VERSION as CURSOR_VERSION,
70
+ ED25519_PUBLIC_KEY_BYTES as ED25519_PUBLIC_KEY_BYTES,
71
+ ED25519_SIGNATURE_BYTES as ED25519_SIGNATURE_BYTES,
72
+ ENVELOPE_VERSION as ENVELOPE_VERSION,
73
+ MAX_CURSOR_LENGTH as MAX_CURSOR_LENGTH,
74
+ MAX_ENVELOPE_BYTES as MAX_ENVELOPE_BYTES,
75
+ MAX_NONCE_LENGTH as MAX_NONCE_LENGTH,
76
+ MAX_PULL_LIMIT as MAX_PULL_LIMIT,
77
+ PUSH_BATCH_LIMIT as PUSH_BATCH_LIMIT,
78
+ REPLAY_WINDOW as REPLAY_WINDOW,
79
+ DeltaKind as DeltaKind,
80
+ PullPage as PullPage,
81
+ PushAccepted as PushAccepted,
82
+ PushReceipt as PushReceipt,
83
+ SyncEnvelope as SyncEnvelope,
84
+ canonical_json as canonical_json,
85
+ content_hash_for as content_hash_for,
86
+ decode_cursor as decode_cursor,
87
+ encode_cursor as encode_cursor,
88
+ initial_cursor as initial_cursor,
89
+ within_replay_window as within_replay_window,
90
+ )
91
+ from agentevo_core.trust import (
92
+ LEGAL_TRANSITIONS as LEGAL_TRANSITIONS,
93
+ TERMINAL_STATES as TERMINAL_STATES,
94
+ ApprovalRecord as ApprovalRecord,
95
+ PromotionRequirement as PromotionRequirement,
96
+ Tombstone as Tombstone,
97
+ TrustState as TrustState,
98
+ check_transition as check_transition,
99
+ may_transition as may_transition,
100
+ promotion_requirement as promotion_requirement,
101
+ )
102
+
103
+ __all__ = [
104
+ "BILLABLE_OPERATIONS",
105
+ "CURSOR_VERSION",
106
+ "DETECTOR_PATTERNS",
107
+ "ED25519_PUBLIC_KEY_BYTES",
108
+ "ED25519_SIGNATURE_BYTES",
109
+ "ENVELOPE_VERSION",
110
+ "FREE_MONTHLY_BILLABLE_REQUESTS",
111
+ "HTTP_STATUS",
112
+ "LEGAL_TRANSITIONS",
113
+ "MAX_ACTION_STRING_LENGTH",
114
+ "MAX_ACTION_TYPE_LENGTH",
115
+ "MAX_BATCH_EVENTS",
116
+ "MAX_CURSOR_LENGTH",
117
+ "MAX_ENVELOPE_BYTES",
118
+ "MAX_IDEMPOTENCY_KEY_LENGTH",
119
+ "MAX_NONCE_LENGTH",
120
+ "MAX_PULL_LIMIT",
121
+ "MAX_TIMEOUT_SECONDS",
122
+ "NON_BILLABLE_OPERATIONS",
123
+ "PUSH_BATCH_LIMIT",
124
+ "REPLAY_WINDOW",
125
+ "TERMINAL_STATES",
126
+ "ActionFallback",
127
+ "ActionIdempotency",
128
+ "ActionTarget",
129
+ "AgentevoError",
130
+ "ApprovalRecord",
131
+ "CanonicalEvent",
132
+ "Classification",
133
+ "Compat",
134
+ "DeltaKind",
135
+ "DetectorPattern",
136
+ "ErrorCode",
137
+ "EventEnvironment",
138
+ "EventProvenance",
139
+ "EventSource",
140
+ "EventType",
141
+ "GuardVerdict",
142
+ "IdempotencyTier",
143
+ "LearnedObject",
144
+ "LearnedObjectType",
145
+ "Ownership",
146
+ "PrivacyMode",
147
+ "PromotionRequirement",
148
+ "PullPage",
149
+ "PushAccepted",
150
+ "PushReceipt",
151
+ "RiskClass",
152
+ "SyncEnvelope",
153
+ "TargetKind",
154
+ "Tombstone",
155
+ "TrustState",
156
+ "TypedAction",
157
+ "Validity",
158
+ "Verification",
159
+ "apply_policy_risk",
160
+ "auto_exec_allowed",
161
+ "auto_retry_allowed",
162
+ "body_conflicts",
163
+ "canonical_json",
164
+ "check_expected_version",
165
+ "check_transition",
166
+ "compute_content_hash",
167
+ "content_hash_for",
168
+ "decode_cursor",
169
+ "encode_cursor",
170
+ "inherit_source",
171
+ "initial_cursor",
172
+ "is_billable",
173
+ "is_high_or_destructive",
174
+ "may_transition",
175
+ "must_redact_before_persistence",
176
+ "promotion_requirement",
177
+ "requires_human_approval",
178
+ "retryable",
179
+ "validate_key",
180
+ "within_replay_window",
181
+ ]
@@ -0,0 +1,323 @@
1
+ """Typed action contract: the executable unit agents propose and guards evaluate.
2
+
3
+ Spec: PRD §20, API §21. Actions are typed data (never prompt strings): nine
4
+ contract fields plus the declared timeout/retry policy. Secrets appear only as
5
+ ``vault://`` references; structural validation lives here, secret-shape
6
+ rejection at the guard boundary (local DLP).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+ from dataclasses import dataclass
16
+ from enum import StrEnum
17
+ from typing import Any
18
+
19
+ from agentevo_core.errors import AgentevoError, ErrorCode
20
+ from agentevo_core.risk import RiskClass
21
+
22
+ #: Longest canonical operation identifier (adapter-mapped tool capability).
23
+ MAX_ACTION_TYPE_LENGTH = 128
24
+
25
+ #: Longest single argument key / permission / condition string.
26
+ MAX_ACTION_STRING_LENGTH = 1024
27
+
28
+ #: Upper bound for a declared step timeout (24h; policy may cap lower).
29
+ MAX_TIMEOUT_SECONDS = 86400.0
30
+
31
+ _ACTION_TYPE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]*\Z")
32
+
33
+
34
+ class ActionIdempotency(StrEnum):
35
+ """Retry semantics the execution engine must honor."""
36
+
37
+ SAFE = "safe"
38
+ BUSINESS_KEY = "business-key"
39
+ UNSAFE = "unsafe"
40
+
41
+
42
+ class ActionFallback(StrEnum):
43
+ """What the runner may do when the deterministic path stops."""
44
+
45
+ STOP = "stop"
46
+ REQUIRE_APPROVAL = "require_approval"
47
+ INVOKE_AGENT = "invoke_agent"
48
+
49
+
50
+ class TargetKind(StrEnum):
51
+ """Closed vocabulary for typed resource references."""
52
+
53
+ PATH = "path"
54
+ REPO = "repo"
55
+ SERVICE = "service"
56
+ API = "api"
57
+ DB = "db"
58
+ NONE = "none"
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class ActionTarget:
63
+ """Typed resource reference. ``NONE`` carries no ``ref``."""
64
+
65
+ kind: TargetKind
66
+ ref: str = ""
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ """JSON-ready mapping (enums as values)."""
70
+ return {"kind": self.kind.value, "ref": self.ref}
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: dict[str, Any]) -> ActionTarget:
74
+ """Parse a mapping; 400 INVALID_ARGUMENT when malformed."""
75
+ if not isinstance(data, dict):
76
+ raise AgentevoError(ErrorCode.INVALID_ARGUMENT, "target must be an object", {})
77
+ try:
78
+ target = cls(kind=TargetKind(data["kind"]), ref=data.get("ref", ""))
79
+ except (KeyError, ValueError, TypeError) as exc:
80
+ raise AgentevoError(ErrorCode.INVALID_ARGUMENT, f"malformed target: {exc}", {}) from exc
81
+ target.validate()
82
+ return target
83
+
84
+ def validate(self) -> None:
85
+ """Raise 400 INVALID_ARGUMENT unless the reference is well-formed."""
86
+ if not isinstance(self.kind, TargetKind):
87
+ raise AgentevoError(
88
+ ErrorCode.INVALID_ARGUMENT,
89
+ f"unknown target kind: {self.kind!r}",
90
+ {"kind": str(self.kind)},
91
+ )
92
+ if self.kind is TargetKind.NONE:
93
+ if self.ref != "":
94
+ raise AgentevoError(
95
+ ErrorCode.INVALID_ARGUMENT,
96
+ "target NONE must not carry a ref",
97
+ {"ref": self.ref[:64]},
98
+ )
99
+ return
100
+ if not self.ref or len(self.ref) > MAX_ACTION_STRING_LENGTH:
101
+ raise AgentevoError(
102
+ ErrorCode.INVALID_ARGUMENT,
103
+ "target ref must be a non-empty string",
104
+ {"kind": self.kind.value},
105
+ )
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class TypedAction:
110
+ """One guard-evaluated action: nine contract fields plus retry policy."""
111
+
112
+ action_type: str
113
+ target: ActionTarget
114
+ arguments: dict[str, Any]
115
+ preconditions: tuple[str, ...]
116
+ postcondition: str | None
117
+ risk_class: RiskClass
118
+ permissions: tuple[str, ...]
119
+ idempotency: ActionIdempotency
120
+ fallback: ActionFallback
121
+ timeout_seconds: float = 60.0
122
+ max_retries: int = 0
123
+
124
+ def validate(self) -> None:
125
+ """Raise 400 INVALID_ARGUMENT unless the action is well-formed."""
126
+ _check_type(self.action_type)
127
+ self.target.validate()
128
+ _check_arguments(self.arguments)
129
+ _check_conditions(self.preconditions, self.postcondition)
130
+ _check_policy_fields(self)
131
+
132
+ def ensure_executable(self) -> None:
133
+ """Raise 400 unless the action may run on the compiled path.
134
+
135
+ A machine-checkable postcondition is required for compiled execution
136
+ (API §21); advisory/interpreted paths may omit it.
137
+ """
138
+ self.validate()
139
+ if not self.postcondition:
140
+ raise AgentevoError(
141
+ ErrorCode.INVALID_ARGUMENT,
142
+ "compiled execution requires a postcondition",
143
+ {"action_type": self.action_type},
144
+ )
145
+
146
+ def to_dict(self) -> dict[str, Any]:
147
+ """JSON-ready mapping (enums as values, tuples as lists)."""
148
+ return {
149
+ "action_type": self.action_type,
150
+ "target": self.target.to_dict(),
151
+ "arguments": self.arguments,
152
+ "preconditions": list(self.preconditions),
153
+ "postcondition": self.postcondition,
154
+ "risk_class": self.risk_class.value,
155
+ "permissions": list(self.permissions),
156
+ "idempotency": self.idempotency.value,
157
+ "fallback": self.fallback.value,
158
+ "timeout_seconds": self.timeout_seconds,
159
+ "max_retries": self.max_retries,
160
+ }
161
+
162
+ @classmethod
163
+ def from_dict(cls, data: dict[str, Any]) -> TypedAction:
164
+ """Parse a mapping; 400 INVALID_ARGUMENT when malformed."""
165
+ if not isinstance(data, dict):
166
+ raise AgentevoError(ErrorCode.INVALID_ARGUMENT, "action must be an object", {})
167
+ for key in ("preconditions", "permissions"):
168
+ value = data.get(key, ())
169
+ if not isinstance(value, (list, tuple)):
170
+ raise AgentevoError(
171
+ ErrorCode.INVALID_ARGUMENT,
172
+ f"action {key} must be a list",
173
+ {"key": key},
174
+ )
175
+ try:
176
+ action = cls(
177
+ action_type=data["action_type"],
178
+ target=ActionTarget.from_dict(data["target"]),
179
+ arguments=data.get("arguments", {}),
180
+ preconditions=tuple(data.get("preconditions", ())),
181
+ postcondition=data.get("postcondition"),
182
+ risk_class=RiskClass(data["risk_class"]),
183
+ permissions=tuple(data.get("permissions", ())),
184
+ idempotency=ActionIdempotency(data["idempotency"]),
185
+ fallback=ActionFallback(data["fallback"]),
186
+ timeout_seconds=data.get("timeout_seconds", 60.0),
187
+ max_retries=data.get("max_retries", 0),
188
+ )
189
+ except (KeyError, ValueError, TypeError) as exc:
190
+ raise AgentevoError(ErrorCode.INVALID_ARGUMENT, f"malformed action: {exc}", {}) from exc
191
+ action.validate()
192
+ return action
193
+
194
+ def canonical_hash(self) -> str:
195
+ """SHA-256 over canonical JSON. Approval tokens bind to this."""
196
+ canonical = {
197
+ "action_type": self.action_type,
198
+ "arguments": self.arguments,
199
+ "fallback": self.fallback.value,
200
+ "idempotency": self.idempotency.value,
201
+ "max_retries": self.max_retries,
202
+ "permissions": list(self.permissions),
203
+ "postcondition": self.postcondition,
204
+ "preconditions": list(self.preconditions),
205
+ "risk_class": self.risk_class.value,
206
+ "target": {"kind": self.target.kind.value, "ref": self.target.ref},
207
+ "timeout_seconds": self.timeout_seconds,
208
+ }
209
+ blob = json.dumps(canonical, sort_keys=True, separators=(",", ":"), default=str)
210
+ return f"sha256:{hashlib.sha256(blob.encode('utf-8')).hexdigest()}"
211
+
212
+
213
+ def _check_type(action_type: str) -> None:
214
+ if (
215
+ not isinstance(action_type, str)
216
+ or not action_type
217
+ or len(action_type) > MAX_ACTION_TYPE_LENGTH
218
+ or _ACTION_TYPE_RE.match(action_type) is None
219
+ ):
220
+ raise AgentevoError(
221
+ ErrorCode.INVALID_ARGUMENT,
222
+ "action_type must be a canonical operation identifier",
223
+ {"action_type": str(action_type)[:64]},
224
+ )
225
+
226
+
227
+ def _check_arguments(arguments: dict[str, Any]) -> None:
228
+ if not isinstance(arguments, dict):
229
+ raise AgentevoError(
230
+ ErrorCode.INVALID_ARGUMENT,
231
+ "arguments must be an object",
232
+ {"type": type(arguments).__name__},
233
+ )
234
+ for key in arguments:
235
+ if not isinstance(key, str) or not key or len(key) > MAX_ACTION_STRING_LENGTH:
236
+ raise AgentevoError(
237
+ ErrorCode.INVALID_ARGUMENT,
238
+ "argument keys must be non-empty strings",
239
+ {"key": str(key)[:64]},
240
+ )
241
+ try:
242
+ json.dumps(arguments, sort_keys=True, separators=(",", ":"))
243
+ except (TypeError, ValueError) as exc:
244
+ raise AgentevoError(
245
+ ErrorCode.INVALID_ARGUMENT,
246
+ f"arguments must be JSON-serializable: {exc}",
247
+ {},
248
+ ) from exc
249
+
250
+
251
+ def _check_conditions(preconditions: tuple[str, ...], postcondition: str | None) -> None:
252
+ for condition in preconditions:
253
+ if not isinstance(condition, str) or not condition:
254
+ raise AgentevoError(
255
+ ErrorCode.INVALID_ARGUMENT,
256
+ "preconditions must be non-empty strings",
257
+ {},
258
+ )
259
+ if postcondition is not None and (not isinstance(postcondition, str) or not postcondition):
260
+ raise AgentevoError(
261
+ ErrorCode.INVALID_ARGUMENT,
262
+ "postcondition must be a non-empty string or null",
263
+ {},
264
+ )
265
+
266
+
267
+ def _check_policy_fields(action: TypedAction) -> None:
268
+ if not isinstance(action.risk_class, RiskClass):
269
+ raise AgentevoError(
270
+ ErrorCode.INVALID_ARGUMENT,
271
+ f"unknown risk class: {action.risk_class!r}",
272
+ {"risk_class": str(action.risk_class)},
273
+ )
274
+ if (
275
+ not action.permissions
276
+ or any(not p or not isinstance(p, str) for p in action.permissions)
277
+ or any(len(p) > MAX_ACTION_STRING_LENGTH for p in action.permissions)
278
+ ):
279
+ raise AgentevoError(
280
+ ErrorCode.INVALID_ARGUMENT,
281
+ "permissions must list the exact required capabilities",
282
+ {"action_type": action.action_type},
283
+ )
284
+ if not isinstance(action.idempotency, ActionIdempotency):
285
+ raise AgentevoError(
286
+ ErrorCode.INVALID_ARGUMENT,
287
+ f"unknown idempotency: {action.idempotency!r}",
288
+ {"idempotency": str(action.idempotency)},
289
+ )
290
+ if not isinstance(action.fallback, ActionFallback):
291
+ raise AgentevoError(
292
+ ErrorCode.INVALID_ARGUMENT,
293
+ f"unknown fallback: {action.fallback!r}",
294
+ {"fallback": str(action.fallback)},
295
+ )
296
+ timeout = action.timeout_seconds
297
+ if (
298
+ not isinstance(timeout, (int, float))
299
+ or isinstance(timeout, bool)
300
+ or not math.isfinite(timeout)
301
+ or timeout <= 0
302
+ or timeout > MAX_TIMEOUT_SECONDS
303
+ ):
304
+ raise AgentevoError(
305
+ ErrorCode.INVALID_ARGUMENT,
306
+ "timeout_seconds must be within (0, 86400]",
307
+ {"action_type": action.action_type},
308
+ )
309
+ retries = action.max_retries
310
+ if not isinstance(retries, int) or isinstance(retries, bool) or retries < 0:
311
+ raise AgentevoError(
312
+ ErrorCode.INVALID_ARGUMENT,
313
+ "max_retries must be a non-negative integer",
314
+ {"action_type": action.action_type},
315
+ )
316
+ if retries > 0 and (
317
+ action.idempotency is ActionIdempotency.UNSAFE or action.risk_class is RiskClass.DESTRUCTIVE
318
+ ):
319
+ raise AgentevoError(
320
+ ErrorCode.INVALID_ARGUMENT,
321
+ "unsafe or destructive actions are never auto-retried",
322
+ {"action_type": action.action_type},
323
+ )
@@ -0,0 +1,55 @@
1
+ """Data classification C0-C4 and SaaS-egress rules.
2
+
3
+ Spec: PRD §7, Data Flow §4, API §18. Embeddings and derivatives inherit the
4
+ classification of their source text. Telemetry schemas are allow-lists with no
5
+ generic payload field.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+
12
+
13
+ class Classification(StrEnum):
14
+ """Sensitivity class. Higher number means more sensitive."""
15
+
16
+ C0 = "C0" # Public: SDK versions, public schemas.
17
+ C1 = "C1" # Control: tenant/user IDs, plan, counters, flags, device public keys.
18
+ C2 = "C2" # Derived sanitized: skill steps, fingerprints, verification summaries.
19
+ C3 = "C3" # Raw operational: prompts, outputs, tool I/O, code, diffs, logs.
20
+ C4 = "C4" # Secrets: tokens, keys, cookies, auth headers. Never a valid payload.
21
+
22
+
23
+ class PrivacyMode(StrEnum):
24
+ """Deployment privacy modes. Local-only is the default."""
25
+
26
+ LOCAL_ONLY = "local_only"
27
+ OPAQUE_RELAY = "opaque_relay" # Cloud stores E2EE envelopes only, no semantic index.
28
+ SEARCHABLE_MANAGED_SYNC = "searchable_managed_sync" # Sanitized C2, tenant-scoped.
29
+ CUSTOMER_VPC = "customer_vpc" # Data plane in the customer VPC, same logical contract.
30
+
31
+
32
+ def inherit_source(source: Classification) -> Classification:
33
+ """Derivatives (embeddings, summaries, fingerprints) inherit source classification."""
34
+ return source
35
+
36
+
37
+ def saas_egress_allowed(payload: Classification, mode: PrivacyMode) -> bool:
38
+ """Whether a payload class may leave the customer boundary for Agentevo SaaS.
39
+
40
+ C4 is never allowed anywhere. C3 has no SaaS endpoint in any mode. C2
41
+ requires an explicit sync mode. C0/C1 are the default SaaS payload.
42
+
43
+ Note: in CUSTOMER_VPC mode the C2/C3 plane runs inside the customer VPC, so
44
+ Agentevo SaaS itself still receives C0/C1 only — hence ``False`` for C2.
45
+ """
46
+ if payload is Classification.C4 or payload is Classification.C3:
47
+ return False
48
+ if payload is Classification.C2:
49
+ return mode in (PrivacyMode.OPAQUE_RELAY, PrivacyMode.SEARCHABLE_MANAGED_SYNC)
50
+ return True
51
+
52
+
53
+ def must_redact_before_persistence(payload: Classification) -> bool:
54
+ """C4 must be replaced by a redaction marker or opaque vault reference."""
55
+ return payload is Classification.C4
@@ -0,0 +1,32 @@
1
+ """Layered environment-compatibility classification.
2
+
3
+ Spec: PRD §18. Compatibility is layered (artifact hashes, dependency-lock
4
+ subset, tool/API/model versions, schema, policy) — never a whole-repo hash.
5
+ Fail closed: high-risk or destructive auto-execution is denied when
6
+ compatibility is UNKNOWN or INCOMPATIBLE. Low/medium risk gets a grace path
7
+ under UNKNOWN; INCOMPATIBLE denies all auto-execution.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from enum import StrEnum
13
+
14
+ from agentevo_core.risk import RiskClass
15
+
16
+
17
+ class Compat(StrEnum):
18
+ """Environment compatibility of a learned object against the current run."""
19
+
20
+ EXACT = "EXACT"
21
+ COMPATIBLE = "COMPATIBLE"
22
+ UNKNOWN = "UNKNOWN"
23
+ INCOMPATIBLE = "INCOMPATIBLE"
24
+
25
+
26
+ def auto_exec_allowed(compat: Compat, risk: RiskClass) -> bool:
27
+ """Fail-closed auto-execution matrix for compatibility vs risk."""
28
+ if compat is Compat.INCOMPATIBLE:
29
+ return False
30
+ if compat is Compat.UNKNOWN:
31
+ return risk in (RiskClass.LOW, RiskClass.MEDIUM)
32
+ return True
@@ -0,0 +1,82 @@
1
+ """Shared C4 detector patterns (single source for both DLP passes).
2
+
3
+ Spec: Data Flow §8, API §12/§17. The local runtime redacts with these
4
+ patterns before egress; the managed server re-scans with the same
5
+ patterns at ingest (second-pass DLP, 422 on C4). Keeping the expressions
6
+ here guarantees the two passes can never drift apart. Entropy heuristics
7
+ stay local-only: the server gate is high-signal patterns, fail-closed,
8
+ with no fuzzy rejection of legitimate compact C2 content.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import NamedTuple
15
+
16
+
17
+ class DetectorPattern(NamedTuple):
18
+ """One named C4 pattern (regex source shared by both passes)."""
19
+
20
+ name: str
21
+ label: str
22
+ expression: str
23
+ flags: int = 0
24
+
25
+
26
+ #: Ordered most-specific-first; on overlap the earlier detector wins.
27
+ DETECTOR_PATTERNS: tuple[DetectorPattern, ...] = (
28
+ DetectorPattern(
29
+ "pem_private_key",
30
+ "private_key",
31
+ r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"
32
+ r".*?-----END [A-Z0-9 ]*PRIVATE KEY-----",
33
+ re.DOTALL,
34
+ ),
35
+ DetectorPattern("aws_access_key", "aws_access_key", r"AKIA[0-9A-Z]{16}"),
36
+ DetectorPattern(
37
+ "aws_secret",
38
+ "aws_secret",
39
+ r"(?i)aws_secret_access_key[\"'\s:=]+[A-Za-z0-9/+=]{40}",
40
+ ),
41
+ DetectorPattern(
42
+ "github_token",
43
+ "github_token",
44
+ r"(?:ghp_[A-Za-z0-9]{8,}|gho_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,})",
45
+ ),
46
+ DetectorPattern("gitlab_token", "gitlab_token", r"glpat-[A-Za-z0-9_-]{8,}"),
47
+ DetectorPattern("anthropic_key", "anthropic_key", r"sk-ant-[A-Za-z0-9_-]{8,}"),
48
+ DetectorPattern("openai_project_key", "openai_key", r"sk-proj-[A-Za-z0-9_-]{8,}"),
49
+ DetectorPattern("openai_key", "openai_key", r"sk-[A-Za-z0-9]{20,}"),
50
+ DetectorPattern("google_api_key", "google_api_key", r"AIza[0-9A-Za-z_-]{35}"),
51
+ DetectorPattern(
52
+ "stripe_secret",
53
+ "stripe_secret",
54
+ r"(?:sk_live|rk_live)_[A-Za-z0-9]{10,}",
55
+ ),
56
+ DetectorPattern("slack_token", "slack_token", r"xox[baprs]-[A-Za-z0-9-]{8,}"),
57
+ DetectorPattern(
58
+ "jwt",
59
+ "jwt",
60
+ r"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}",
61
+ ),
62
+ DetectorPattern(
63
+ "bearer_token",
64
+ "bearer_token",
65
+ r"(?i)Bearer\s+[A-Za-z0-9._~+/-]{8,}",
66
+ ),
67
+ DetectorPattern("basic_auth", "basic_auth", r"(?i)Basic\s+[A-Za-z0-9+/=]{8,}"),
68
+ DetectorPattern(
69
+ "url_password",
70
+ "url_password",
71
+ r"(?i)([a-z][a-z0-9+.-]*://[^/\s:@]+:)([^/\s@]{1,})@",
72
+ ),
73
+ DetectorPattern(
74
+ "password_assignment",
75
+ "password",
76
+ # Requires an explicit : or = separator so prose ("secret sauce") and
77
+ # bare commands ("pwd /home/user") do not match. JSON, env, YAML, and
78
+ # header assignments ("password": "...", API_KEY=..., key: ...) match.
79
+ r"(?i)(?:password|passwd|pwd|secret|api[_-]?key|auth[_-]?token|"
80
+ r"access[_-]?token|session(?:id|[_-]?token)?)[\"']?\s*[:=]\s*[\"']?[^\s\"',;}]{4,}",
81
+ ),
82
+ )