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.
- agentevo_core/__init__.py +181 -0
- agentevo_core/actions.py +323 -0
- agentevo_core/classification.py +55 -0
- agentevo_core/compat.py +32 -0
- agentevo_core/dlp_patterns.py +82 -0
- agentevo_core/errors.py +83 -0
- agentevo_core/events.py +203 -0
- agentevo_core/idempotency.py +63 -0
- agentevo_core/metering.py +68 -0
- agentevo_core/objects.py +150 -0
- agentevo_core/risk.py +54 -0
- agentevo_core/sync.py +601 -0
- agentevo_core/trust.py +179 -0
- agentevo_core-0.1.0.dist-info/METADATA +232 -0
- agentevo_core-0.1.0.dist-info/RECORD +17 -0
- agentevo_core-0.1.0.dist-info/WHEEL +4 -0
- agentevo_core-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -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
|
+
]
|
agentevo_core/actions.py
ADDED
|
@@ -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
|
agentevo_core/compat.py
ADDED
|
@@ -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
|
+
)
|