cognition-system-behavior-contracts 0.7.0__tar.gz
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.
- cognition_system_behavior_contracts-0.7.0/PKG-INFO +9 -0
- cognition_system_behavior_contracts-0.7.0/pyproject.toml +19 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/__init__.py +67 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/adk_tool.py +127 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/governance_candidate.py +467 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/llm_invocation.py +18 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/product_gateway_response_summary.py +326 -0
- cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/runtime.py +106 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cognition-system-behavior-contracts
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Behavior contracts for Cognition System.
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: cognition-system-schemas==0.7.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
Cognition System v0.7.0 public package. See the version-specific public README in the release repository for usage and boundary notes.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cognition-system-behavior-contracts"
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
readme = { text = "Cognition System v0.7.0 public package. See the version-specific public README in the release repository for usage and boundary notes.", content-type = "text/markdown" }
|
|
5
|
+
description = "Behavior contracts for Cognition System."
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"cognition-system-schemas==0.7.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/behavior_contracts"]
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
cognition-system-schemas = { workspace = true }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Behavior contracts for Cognition Engine."""
|
|
2
|
+
|
|
3
|
+
from behavior_contracts.adk_tool import (
|
|
4
|
+
assert_controlled_live_tool_requires_explicit_confirmation,
|
|
5
|
+
assert_low_risk_tool_requires_no_external_side_effects,
|
|
6
|
+
assert_no_raw_adk_or_tool_payload,
|
|
7
|
+
assert_tool_audit_is_sanitized,
|
|
8
|
+
assert_tool_consumer_is_candidate_only,
|
|
9
|
+
)
|
|
10
|
+
from behavior_contracts.governance_candidate import (
|
|
11
|
+
CandidateGuardResult,
|
|
12
|
+
CandidateOnlyGuard,
|
|
13
|
+
NoAdkNativeObjectLeakageGuard,
|
|
14
|
+
NoExecutionGuard,
|
|
15
|
+
NoReleaseActionGuard,
|
|
16
|
+
NoRuntimeActionGuard,
|
|
17
|
+
OperatorConfirmationRequiredGuard,
|
|
18
|
+
ReviewerExecutorSeparationGuard,
|
|
19
|
+
SensitiveOutputRedactionGuard,
|
|
20
|
+
validate_governance_candidate_guards,
|
|
21
|
+
)
|
|
22
|
+
from behavior_contracts.llm_invocation import GovernedLlmInvocationService
|
|
23
|
+
from behavior_contracts.product_gateway_response_summary import (
|
|
24
|
+
DEFAULT_PRODUCT_GATEWAY_RESPONSE_SUMMARY_GUARDS,
|
|
25
|
+
ProductGatewayResponseBlockedRequiresReasonGuard,
|
|
26
|
+
ProductGatewayResponseNoExecutionGuard,
|
|
27
|
+
ProductGatewayResponseNoRawPayloadGuard,
|
|
28
|
+
ProductGatewayResponseNoRuntimeObjectLeakageGuard,
|
|
29
|
+
ProductGatewayResponseRefsOnlyGuard,
|
|
30
|
+
ProductGatewayResponseSummaryHeaderGuard,
|
|
31
|
+
ProductGatewayResponseSummaryOnlyGuard,
|
|
32
|
+
validate_product_gateway_response_summary_guards,
|
|
33
|
+
)
|
|
34
|
+
from behavior_contracts.runtime import (
|
|
35
|
+
AdkServiceFactsProvider,
|
|
36
|
+
RecordedRunEvidenceProvider,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"assert_controlled_live_tool_requires_explicit_confirmation",
|
|
41
|
+
"assert_low_risk_tool_requires_no_external_side_effects",
|
|
42
|
+
"assert_no_raw_adk_or_tool_payload",
|
|
43
|
+
"assert_tool_audit_is_sanitized",
|
|
44
|
+
"assert_tool_consumer_is_candidate_only",
|
|
45
|
+
"CandidateGuardResult",
|
|
46
|
+
"CandidateOnlyGuard",
|
|
47
|
+
"DEFAULT_PRODUCT_GATEWAY_RESPONSE_SUMMARY_GUARDS",
|
|
48
|
+
"GovernedLlmInvocationService",
|
|
49
|
+
"NoAdkNativeObjectLeakageGuard",
|
|
50
|
+
"NoExecutionGuard",
|
|
51
|
+
"NoReleaseActionGuard",
|
|
52
|
+
"NoRuntimeActionGuard",
|
|
53
|
+
"OperatorConfirmationRequiredGuard",
|
|
54
|
+
"ProductGatewayResponseBlockedRequiresReasonGuard",
|
|
55
|
+
"ProductGatewayResponseNoExecutionGuard",
|
|
56
|
+
"ProductGatewayResponseNoRawPayloadGuard",
|
|
57
|
+
"ProductGatewayResponseNoRuntimeObjectLeakageGuard",
|
|
58
|
+
"ProductGatewayResponseRefsOnlyGuard",
|
|
59
|
+
"ProductGatewayResponseSummaryHeaderGuard",
|
|
60
|
+
"ProductGatewayResponseSummaryOnlyGuard",
|
|
61
|
+
"ReviewerExecutorSeparationGuard",
|
|
62
|
+
"AdkServiceFactsProvider",
|
|
63
|
+
"RecordedRunEvidenceProvider",
|
|
64
|
+
"SensitiveOutputRedactionGuard",
|
|
65
|
+
"validate_product_gateway_response_summary_guards",
|
|
66
|
+
"validate_governance_candidate_guards",
|
|
67
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Behavior contracts for ADK native FunctionTool product boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from behavior_contracts.governance_candidate import CandidateGuardResult
|
|
8
|
+
from schemas.adk_tool import (
|
|
9
|
+
AdkFunctionToolAuditContract,
|
|
10
|
+
AdkFunctionToolAuditStatus,
|
|
11
|
+
AdkFunctionToolRiskProfile,
|
|
12
|
+
ToolRiskLevel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assert_tool_audit_is_sanitized(
|
|
17
|
+
audit: AdkFunctionToolAuditContract | Mapping[str, Any],
|
|
18
|
+
) -> CandidateGuardResult:
|
|
19
|
+
"""Validate that public Tool audit facts do not expose raw runtime payloads."""
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
AdkFunctionToolAuditContract.model_validate(audit)
|
|
23
|
+
except ValueError as exc:
|
|
24
|
+
return CandidateGuardResult(False, (str(exc),))
|
|
25
|
+
return CandidateGuardResult(True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def assert_low_risk_tool_requires_no_external_side_effects(
|
|
29
|
+
risk_profile: AdkFunctionToolRiskProfile | Mapping[str, Any],
|
|
30
|
+
) -> CandidateGuardResult:
|
|
31
|
+
"""Validate the low-risk Tool profile boundary."""
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
profile = AdkFunctionToolRiskProfile.model_validate(risk_profile)
|
|
35
|
+
except ValueError as exc:
|
|
36
|
+
return CandidateGuardResult(False, (str(exc),))
|
|
37
|
+
if profile.risk_level is not ToolRiskLevel.LOW:
|
|
38
|
+
return CandidateGuardResult(False, ("risk_level must be low.",))
|
|
39
|
+
return CandidateGuardResult(True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assert_controlled_live_tool_requires_explicit_confirmation(
|
|
43
|
+
audit: AdkFunctionToolAuditContract | Mapping[str, Any],
|
|
44
|
+
) -> CandidateGuardResult:
|
|
45
|
+
"""Require explicit confirmation before controlled-live Tool execution."""
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
contract = AdkFunctionToolAuditContract.model_validate(audit)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
return CandidateGuardResult(False, (str(exc),))
|
|
51
|
+
violations: list[str] = []
|
|
52
|
+
if contract.tool_runtime_call_performed:
|
|
53
|
+
if not contract.tool_confirmation_required:
|
|
54
|
+
violations.append("tool_confirmation_required must be true.")
|
|
55
|
+
if not contract.tool_confirmation_granted:
|
|
56
|
+
violations.append("tool_confirmation_granted must be true.")
|
|
57
|
+
if not contract.tool_confirmation_decision_source:
|
|
58
|
+
violations.append("tool_confirmation_decision_source is required.")
|
|
59
|
+
return CandidateGuardResult(not violations, tuple(violations))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def assert_tool_consumer_is_candidate_only(
|
|
63
|
+
consumer_facts: Mapping[str, Any],
|
|
64
|
+
) -> CandidateGuardResult:
|
|
65
|
+
"""Require Tool consumers to remain read-only candidate consumers."""
|
|
66
|
+
|
|
67
|
+
violations: list[str] = []
|
|
68
|
+
if consumer_facts.get("candidate_only") is not True:
|
|
69
|
+
violations.append("candidate_only must be true.")
|
|
70
|
+
for field_name in (
|
|
71
|
+
"tool_execution_enabled",
|
|
72
|
+
"tool_control_plane_enabled",
|
|
73
|
+
"runtime_execution_enabled",
|
|
74
|
+
"formal_governance_decision_enabled",
|
|
75
|
+
):
|
|
76
|
+
if consumer_facts.get(field_name) is True:
|
|
77
|
+
violations.append(f"{field_name} must not be true.")
|
|
78
|
+
return CandidateGuardResult(not violations, tuple(violations))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def assert_no_raw_adk_or_tool_payload(
|
|
82
|
+
value: Mapping[str, Any],
|
|
83
|
+
) -> CandidateGuardResult:
|
|
84
|
+
"""Reject raw ADK, ToolContext, ToolConfirmation, input, and output payloads."""
|
|
85
|
+
|
|
86
|
+
violations = [
|
|
87
|
+
f"raw ADK or tool payload is forbidden at {path}."
|
|
88
|
+
for path, item in _walk(value)
|
|
89
|
+
if _is_raw_tool_payload(path, item)
|
|
90
|
+
]
|
|
91
|
+
return CandidateGuardResult(not violations, tuple(violations))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _walk(value: Any, path: str = "$") -> list[tuple[str, Any]]:
|
|
95
|
+
items = [(path, value)]
|
|
96
|
+
if isinstance(value, Mapping):
|
|
97
|
+
for key, item in value.items():
|
|
98
|
+
items.extend(_walk(item, f"{path}.{key}"))
|
|
99
|
+
elif isinstance(value, (list, tuple)):
|
|
100
|
+
for index, item in enumerate(value):
|
|
101
|
+
items.extend(_walk(item, f"{path}[{index}]"))
|
|
102
|
+
return items
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_raw_tool_payload(path: str, value: Any) -> bool:
|
|
106
|
+
key = path.rsplit(".", maxsplit=1)[-1].lower()
|
|
107
|
+
if key in {
|
|
108
|
+
"adk_object",
|
|
109
|
+
"function_tool",
|
|
110
|
+
"raw",
|
|
111
|
+
"raw_adk_object",
|
|
112
|
+
"raw_input",
|
|
113
|
+
"raw_output",
|
|
114
|
+
"raw_tool_input",
|
|
115
|
+
"raw_tool_output",
|
|
116
|
+
"tool_confirmation",
|
|
117
|
+
"tool_context",
|
|
118
|
+
"tool_input",
|
|
119
|
+
"tool_output",
|
|
120
|
+
}:
|
|
121
|
+
return True
|
|
122
|
+
if isinstance(value, Mapping):
|
|
123
|
+
module_name = value.get("object_module")
|
|
124
|
+
return isinstance(module_name, str) and module_name.startswith("google.adk")
|
|
125
|
+
if value is None or isinstance(value, (str, int, float, bool, list, tuple, dict)):
|
|
126
|
+
return False
|
|
127
|
+
return type(value).__module__.startswith("google.adk")
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Governance candidate behavior guards.
|
|
2
|
+
|
|
3
|
+
These guards describe safety invariants for governance candidates. They do not
|
|
4
|
+
execute release, runtime, policy, or governance actions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
FORBIDDEN_RELEASE_ACTIONS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"release",
|
|
16
|
+
"block",
|
|
17
|
+
"pass",
|
|
18
|
+
"publish",
|
|
19
|
+
"upload",
|
|
20
|
+
"twine_upload",
|
|
21
|
+
"git_tag",
|
|
22
|
+
"git_push",
|
|
23
|
+
"github_release",
|
|
24
|
+
"trusted_publishing",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
FORBIDDEN_RUNTIME_ACTIONS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"runtime_fix",
|
|
31
|
+
"run_config_update",
|
|
32
|
+
"service_bundle_update",
|
|
33
|
+
"execute_workflow",
|
|
34
|
+
"call_runtime_container",
|
|
35
|
+
"call_composition",
|
|
36
|
+
"call_adk_adapter",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
FORBIDDEN_RUNTIME_OBJECT_MODULE_PREFIXES = (
|
|
41
|
+
"google.adk",
|
|
42
|
+
"adk_adapter",
|
|
43
|
+
"composition",
|
|
44
|
+
"runtime_container",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
SENSITIVE_OUTPUT_KEYS = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"command_output",
|
|
50
|
+
"command_outputs",
|
|
51
|
+
"credential",
|
|
52
|
+
"credentials",
|
|
53
|
+
"env",
|
|
54
|
+
"raw",
|
|
55
|
+
"raw_output",
|
|
56
|
+
"secret",
|
|
57
|
+
"stderr",
|
|
58
|
+
"stdout",
|
|
59
|
+
"token",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
SENSITIVE_KEY_EXCEPTIONS = frozenset(
|
|
64
|
+
{
|
|
65
|
+
"raw_output_digest",
|
|
66
|
+
"sensitive_fields_omitted",
|
|
67
|
+
"token_presence_check_mode",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_POLICY_DOMAIN = "product_agent_output_governance"
|
|
72
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_CASE_TYPE = "product_agent_output_governance_review"
|
|
73
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_DECISION_CANDIDATE_SCOPE = (
|
|
74
|
+
"product_agent_output_governance_decision_candidate"
|
|
75
|
+
)
|
|
76
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_ALLOWED_DOMAIN_METADATA_KEYS = frozenset(
|
|
77
|
+
{
|
|
78
|
+
"product_gateway_request_id",
|
|
79
|
+
"product_gateway_entry_kind",
|
|
80
|
+
"product_gateway_status",
|
|
81
|
+
"product_gateway_exit_code",
|
|
82
|
+
"agent_advice_candidate_id",
|
|
83
|
+
"agent_advice_status",
|
|
84
|
+
"agent_advice_recommendation",
|
|
85
|
+
"ready_for_review",
|
|
86
|
+
"evidence_statuses",
|
|
87
|
+
"missing_evidence",
|
|
88
|
+
"warning_candidates",
|
|
89
|
+
"block_candidates",
|
|
90
|
+
"human_review_reasons",
|
|
91
|
+
"summary_only",
|
|
92
|
+
"refs_only",
|
|
93
|
+
"candidate_only",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_BOUNDARY_FLAGS = frozenset(
|
|
97
|
+
{
|
|
98
|
+
"summary_only",
|
|
99
|
+
"refs_only",
|
|
100
|
+
"candidate_only",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_KEYS = frozenset(
|
|
104
|
+
{
|
|
105
|
+
"agent_task_advice_consumption_payload",
|
|
106
|
+
"api_key",
|
|
107
|
+
"artifact_content",
|
|
108
|
+
"completion",
|
|
109
|
+
"credential",
|
|
110
|
+
"credentials",
|
|
111
|
+
"full_response",
|
|
112
|
+
"message",
|
|
113
|
+
"messages",
|
|
114
|
+
"payload",
|
|
115
|
+
"product_gateway_request",
|
|
116
|
+
"product_gateway_response",
|
|
117
|
+
"prompt",
|
|
118
|
+
"provider_payload",
|
|
119
|
+
"provider_response",
|
|
120
|
+
"raw",
|
|
121
|
+
"raw_adk_object",
|
|
122
|
+
"raw_api_payload",
|
|
123
|
+
"raw_input",
|
|
124
|
+
"raw_output",
|
|
125
|
+
"raw_payload",
|
|
126
|
+
"raw_prompt",
|
|
127
|
+
"raw_provider_payload",
|
|
128
|
+
"raw_provider_response",
|
|
129
|
+
"raw_response",
|
|
130
|
+
"raw_tool_input",
|
|
131
|
+
"raw_tool_output",
|
|
132
|
+
"raw_user_message",
|
|
133
|
+
"response",
|
|
134
|
+
"response_text",
|
|
135
|
+
"secret",
|
|
136
|
+
"system_prompt",
|
|
137
|
+
"text",
|
|
138
|
+
"token",
|
|
139
|
+
"tool_context",
|
|
140
|
+
"tool_input",
|
|
141
|
+
"tool_output",
|
|
142
|
+
"user_message",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_ACTION_FIELDS = frozenset(
|
|
146
|
+
{
|
|
147
|
+
"action_kind",
|
|
148
|
+
"can_publish",
|
|
149
|
+
"can_release",
|
|
150
|
+
"execution_result",
|
|
151
|
+
"release_action_kind",
|
|
152
|
+
"release_action_result",
|
|
153
|
+
"runtime_action_kind",
|
|
154
|
+
"tag_release_and_publish",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_FALSE_INVARIANT_FIELDS = (
|
|
158
|
+
"execution_enabled",
|
|
159
|
+
"formal_decision_enabled",
|
|
160
|
+
"formal_outcome_enabled",
|
|
161
|
+
"governance_outcome_enabled",
|
|
162
|
+
"policy_execution_enabled",
|
|
163
|
+
"release_action_enabled",
|
|
164
|
+
"runtime_execution_enabled",
|
|
165
|
+
)
|
|
166
|
+
PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_RELEASE_REASON = (
|
|
167
|
+
"Release action boundary review is pending."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class CandidateGuardResult:
|
|
173
|
+
"""Result returned by a governance candidate guard."""
|
|
174
|
+
|
|
175
|
+
passed: bool
|
|
176
|
+
violations: tuple[str, ...] = field(default_factory=tuple)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class CandidateGuard:
|
|
180
|
+
"""Base class for non-executing governance candidate guards."""
|
|
181
|
+
|
|
182
|
+
guard_name = "candidate_guard"
|
|
183
|
+
|
|
184
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
185
|
+
"""Validate a candidate mapping without executing any action."""
|
|
186
|
+
|
|
187
|
+
raise NotImplementedError
|
|
188
|
+
|
|
189
|
+
def _result(self, violations: list[str]) -> CandidateGuardResult:
|
|
190
|
+
return CandidateGuardResult(
|
|
191
|
+
passed=not violations,
|
|
192
|
+
violations=tuple(violations),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class CandidateOnlyGuard(CandidateGuard):
|
|
197
|
+
"""Require explicit candidate-only semantics."""
|
|
198
|
+
|
|
199
|
+
guard_name = "candidate_only_guard"
|
|
200
|
+
|
|
201
|
+
_semantic_fields = (
|
|
202
|
+
"action_semantics",
|
|
203
|
+
"decision_semantics",
|
|
204
|
+
"outcome_semantics",
|
|
205
|
+
"review_semantics",
|
|
206
|
+
"policy_status",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
210
|
+
values = [candidate.get(field_name) for field_name in self._semantic_fields]
|
|
211
|
+
has_candidate_scope = bool(candidate.get("candidate_scope"))
|
|
212
|
+
is_explicit_candidate = candidate.get("candidate_only") is True
|
|
213
|
+
if "candidate_only" in values or has_candidate_scope or is_explicit_candidate:
|
|
214
|
+
return self._result([])
|
|
215
|
+
return self._result(["Candidate must declare candidate_only semantics."])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class NoExecutionGuard(CandidateGuard):
|
|
219
|
+
"""Ensure candidate objects cannot enable execution."""
|
|
220
|
+
|
|
221
|
+
guard_name = "no_execution_guard"
|
|
222
|
+
|
|
223
|
+
_false_invariant_fields = (
|
|
224
|
+
"execution_enabled",
|
|
225
|
+
"formal_decision_enabled",
|
|
226
|
+
"formal_outcome_enabled",
|
|
227
|
+
"governance_outcome_enabled",
|
|
228
|
+
"policy_execution_enabled",
|
|
229
|
+
"release_action_enabled",
|
|
230
|
+
"runtime_execution_enabled",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
234
|
+
violations = [
|
|
235
|
+
f"{field_name} must not be true."
|
|
236
|
+
for field_name in self._false_invariant_fields
|
|
237
|
+
if candidate.get(field_name) is True
|
|
238
|
+
]
|
|
239
|
+
return self._result(violations)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class OperatorConfirmationRequiredGuard(CandidateGuard):
|
|
243
|
+
"""Require operator confirmation before any external action boundary."""
|
|
244
|
+
|
|
245
|
+
guard_name = "operator_confirmation_required_guard"
|
|
246
|
+
|
|
247
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
248
|
+
if candidate.get("requires_operator_confirmation") is True:
|
|
249
|
+
return self._result([])
|
|
250
|
+
return self._result(["requires_operator_confirmation must be true."])
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ReviewerExecutorSeparationGuard(CandidateGuard):
|
|
254
|
+
"""Require reviewer and executor identities to remain separate."""
|
|
255
|
+
|
|
256
|
+
guard_name = "reviewer_executor_separation_guard"
|
|
257
|
+
|
|
258
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
259
|
+
reviewer = candidate.get("reviewer")
|
|
260
|
+
executor = candidate.get("executor")
|
|
261
|
+
if reviewer and executor and reviewer == executor:
|
|
262
|
+
return self._result(["reviewer and executor must be separate."])
|
|
263
|
+
return self._result([])
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class NoReleaseActionGuard(CandidateGuard):
|
|
267
|
+
"""Reject formal release, block, pass, and publishing action names."""
|
|
268
|
+
|
|
269
|
+
guard_name = "no_release_action_guard"
|
|
270
|
+
|
|
271
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
272
|
+
values = _candidate_action_values(candidate)
|
|
273
|
+
forbidden = sorted(value for value in values if value in FORBIDDEN_RELEASE_ACTIONS)
|
|
274
|
+
return self._result(
|
|
275
|
+
[f"Release action is forbidden: {value}." for value in forbidden]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class NoRuntimeActionGuard(CandidateGuard):
|
|
280
|
+
"""Reject formal runtime fix and runtime update action names."""
|
|
281
|
+
|
|
282
|
+
guard_name = "no_runtime_action_guard"
|
|
283
|
+
|
|
284
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
285
|
+
values = _candidate_action_values(candidate)
|
|
286
|
+
forbidden = sorted(value for value in values if value in FORBIDDEN_RUNTIME_ACTIONS)
|
|
287
|
+
return self._result(
|
|
288
|
+
[f"Runtime action is forbidden: {value}." for value in forbidden]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class NoAdkNativeObjectLeakageGuard(CandidateGuard):
|
|
293
|
+
"""Reject ADK and execution-layer object leakage."""
|
|
294
|
+
|
|
295
|
+
guard_name = "no_adk_native_object_leakage_guard"
|
|
296
|
+
|
|
297
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
298
|
+
violations = [
|
|
299
|
+
f"Runtime object leakage is forbidden at {path}."
|
|
300
|
+
for path, value in _walk(candidate)
|
|
301
|
+
if _is_runtime_object(value)
|
|
302
|
+
]
|
|
303
|
+
return self._result(violations)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class SensitiveOutputRedactionGuard(CandidateGuard):
|
|
307
|
+
"""Reject raw or sensitive output fields in public candidate boundaries."""
|
|
308
|
+
|
|
309
|
+
guard_name = "sensitive_output_redaction_guard"
|
|
310
|
+
|
|
311
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
312
|
+
violations = [
|
|
313
|
+
f"Sensitive output key is forbidden at {path}."
|
|
314
|
+
for path, _value in _walk(candidate)
|
|
315
|
+
if _is_sensitive_path(path)
|
|
316
|
+
]
|
|
317
|
+
return self._result(violations)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class ProductAgentOutputGovernanceDomainGuard(CandidateGuard):
|
|
321
|
+
"""Enforce product-agent output governance candidate boundaries."""
|
|
322
|
+
|
|
323
|
+
guard_name = "product_agent_output_governance_domain_guard"
|
|
324
|
+
|
|
325
|
+
def validate(self, candidate: Mapping[str, Any]) -> CandidateGuardResult:
|
|
326
|
+
if not _is_product_agent_output_governance_candidate(candidate):
|
|
327
|
+
return self._result([])
|
|
328
|
+
|
|
329
|
+
violations: list[str] = []
|
|
330
|
+
domain_metadata = candidate.get("domain_metadata", {})
|
|
331
|
+
if "domain_metadata" in candidate and not isinstance(domain_metadata, Mapping):
|
|
332
|
+
violations.append("domain_metadata must be a mapping.")
|
|
333
|
+
elif isinstance(domain_metadata, Mapping):
|
|
334
|
+
unexpected_keys = sorted(
|
|
335
|
+
str(key)
|
|
336
|
+
for key in domain_metadata
|
|
337
|
+
if str(key)
|
|
338
|
+
not in PRODUCT_AGENT_OUTPUT_GOVERNANCE_ALLOWED_DOMAIN_METADATA_KEYS
|
|
339
|
+
)
|
|
340
|
+
violations.extend(
|
|
341
|
+
f"Product-agent domain_metadata key is not allowed: {key}."
|
|
342
|
+
for key in unexpected_keys
|
|
343
|
+
)
|
|
344
|
+
for flag in PRODUCT_AGENT_OUTPUT_GOVERNANCE_BOUNDARY_FLAGS:
|
|
345
|
+
if flag in domain_metadata and domain_metadata.get(flag) is not True:
|
|
346
|
+
violations.append(f"{flag} must be true for product-agent candidates.")
|
|
347
|
+
|
|
348
|
+
violations.extend(
|
|
349
|
+
f"{field_name} must not be true for product-agent candidates."
|
|
350
|
+
for field_name in PRODUCT_AGENT_OUTPUT_GOVERNANCE_FALSE_INVARIANT_FIELDS
|
|
351
|
+
if candidate.get(field_name) is True
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
for path, value in _walk(candidate):
|
|
355
|
+
key = _path_key(path)
|
|
356
|
+
if key in PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_KEYS:
|
|
357
|
+
violations.append(
|
|
358
|
+
f"Product-agent raw or sensitive key is forbidden at {path}."
|
|
359
|
+
)
|
|
360
|
+
if key in PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_ACTION_FIELDS:
|
|
361
|
+
violations.append(
|
|
362
|
+
f"Product-agent action field is forbidden at {path}."
|
|
363
|
+
)
|
|
364
|
+
if (
|
|
365
|
+
isinstance(value, str)
|
|
366
|
+
and PRODUCT_AGENT_OUTPUT_GOVERNANCE_FORBIDDEN_RELEASE_REASON in value
|
|
367
|
+
):
|
|
368
|
+
violations.append(
|
|
369
|
+
"Product-agent candidate must not use release action boundary "
|
|
370
|
+
f"reason at {path}."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return self._result(violations)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
DEFAULT_GOVERNANCE_CANDIDATE_GUARDS = (
|
|
377
|
+
CandidateOnlyGuard(),
|
|
378
|
+
NoExecutionGuard(),
|
|
379
|
+
OperatorConfirmationRequiredGuard(),
|
|
380
|
+
ReviewerExecutorSeparationGuard(),
|
|
381
|
+
NoReleaseActionGuard(),
|
|
382
|
+
NoRuntimeActionGuard(),
|
|
383
|
+
ProductAgentOutputGovernanceDomainGuard(),
|
|
384
|
+
NoAdkNativeObjectLeakageGuard(),
|
|
385
|
+
SensitiveOutputRedactionGuard(),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def validate_governance_candidate_guards(
|
|
390
|
+
candidate: Mapping[str, Any],
|
|
391
|
+
guards: tuple[CandidateGuard, ...] = DEFAULT_GOVERNANCE_CANDIDATE_GUARDS,
|
|
392
|
+
) -> CandidateGuardResult:
|
|
393
|
+
"""Run candidate guards and return a combined non-executing result."""
|
|
394
|
+
|
|
395
|
+
violations: list[str] = []
|
|
396
|
+
for guard in guards:
|
|
397
|
+
result = guard.validate(candidate)
|
|
398
|
+
violations.extend(f"{guard.guard_name}: {item}" for item in result.violations)
|
|
399
|
+
return CandidateGuardResult(
|
|
400
|
+
passed=not violations,
|
|
401
|
+
violations=tuple(violations),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _candidate_action_values(candidate: Mapping[str, Any]) -> set[str]:
|
|
406
|
+
values: set[str] = set()
|
|
407
|
+
for key in ("action_kind", "decision", "runtime_action_kind", "release_action_kind"):
|
|
408
|
+
value = candidate.get(key)
|
|
409
|
+
if isinstance(value, str):
|
|
410
|
+
values.add(value.lower())
|
|
411
|
+
for key in ("allowed_action_kinds", "forbidden_action_kinds"):
|
|
412
|
+
value = candidate.get(key)
|
|
413
|
+
if isinstance(value, (list, tuple, set)):
|
|
414
|
+
values.update(item.lower() for item in value if isinstance(item, str))
|
|
415
|
+
return values
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _walk(value: Any, path: str = "$") -> list[tuple[str, Any]]:
|
|
419
|
+
items = [(path, value)]
|
|
420
|
+
if isinstance(value, Mapping):
|
|
421
|
+
for key, item in value.items():
|
|
422
|
+
items.extend(_walk(item, f"{path}.{key}"))
|
|
423
|
+
elif isinstance(value, (list, tuple)):
|
|
424
|
+
for index, item in enumerate(value):
|
|
425
|
+
items.extend(_walk(item, f"{path}[{index}]"))
|
|
426
|
+
return items
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _is_runtime_object(value: Any) -> bool:
|
|
430
|
+
if isinstance(value, Mapping):
|
|
431
|
+
module_name = value.get("object_module")
|
|
432
|
+
return isinstance(module_name, str) and module_name.startswith(
|
|
433
|
+
FORBIDDEN_RUNTIME_OBJECT_MODULE_PREFIXES
|
|
434
|
+
)
|
|
435
|
+
if value is None or isinstance(value, (str, int, float, bool, list, tuple, dict)):
|
|
436
|
+
return False
|
|
437
|
+
return type(value).__module__.startswith(FORBIDDEN_RUNTIME_OBJECT_MODULE_PREFIXES)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _is_sensitive_path(path: str) -> bool:
|
|
441
|
+
key = path.rsplit(".", maxsplit=1)[-1].lower()
|
|
442
|
+
if key in SENSITIVE_KEY_EXCEPTIONS:
|
|
443
|
+
return False
|
|
444
|
+
return (
|
|
445
|
+
key in SENSITIVE_OUTPUT_KEYS
|
|
446
|
+
or key.endswith("_token")
|
|
447
|
+
or key.endswith("_credential")
|
|
448
|
+
or key.endswith("_secret")
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _is_product_agent_output_governance_candidate(
|
|
453
|
+
candidate: Mapping[str, Any],
|
|
454
|
+
) -> bool:
|
|
455
|
+
return (
|
|
456
|
+
candidate.get("policy_domain") == PRODUCT_AGENT_OUTPUT_GOVERNANCE_POLICY_DOMAIN
|
|
457
|
+
or candidate.get("case_type") == PRODUCT_AGENT_OUTPUT_GOVERNANCE_CASE_TYPE
|
|
458
|
+
or candidate.get("candidate_scope")
|
|
459
|
+
== PRODUCT_AGENT_OUTPUT_GOVERNANCE_DECISION_CANDIDATE_SCOPE
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _path_key(path: str) -> str:
|
|
464
|
+
key = path.rsplit(".", maxsplit=1)[-1]
|
|
465
|
+
if "[" in key:
|
|
466
|
+
key = key.split("[", maxsplit=1)[0]
|
|
467
|
+
return key.lower()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Behavior contract for governed LLM invocation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from schemas.llm_invocation import LlmInvocationRequest, LlmInvocationResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GovernedLlmInvocationService(Protocol):
|
|
11
|
+
"""Protocol for a governed LLM invocation capability.
|
|
12
|
+
|
|
13
|
+
Implementations may use ADK LiteLlm, LiteLLM provider routes, or an ADK
|
|
14
|
+
WorkflowRunner chain. The protocol itself does not perform model calls.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def invoke(self, request: LlmInvocationRequest) -> LlmInvocationResult:
|
|
18
|
+
"""Run a governed invocation and return sanitized result facts."""
|
cognition_system_behavior_contracts-0.7.0/src/behavior_contracts/product_gateway_response_summary.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Behavior guards for product gateway response summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from behavior_contracts.governance_candidate import CandidateGuardResult
|
|
8
|
+
from schemas.product_gateway_response_summary import (
|
|
9
|
+
PRODUCT_GATEWAY_RESPONSE_SUMMARY_ENTRY_KINDS,
|
|
10
|
+
PRODUCT_GATEWAY_RESPONSE_SUMMARY_PAYLOAD_TYPE,
|
|
11
|
+
PRODUCT_GATEWAY_RESPONSE_SUMMARY_PRODUCT,
|
|
12
|
+
PRODUCT_GATEWAY_RESPONSE_SUMMARY_STATUSES,
|
|
13
|
+
PRODUCT_GATEWAY_RESPONSE_SUMMARY_VERSION,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SUMMARY_ONLY_FORBIDDEN_KEYS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"artifact_content",
|
|
20
|
+
"completion",
|
|
21
|
+
"content",
|
|
22
|
+
"full_response",
|
|
23
|
+
"message",
|
|
24
|
+
"messages",
|
|
25
|
+
"prompt",
|
|
26
|
+
"response",
|
|
27
|
+
"response_text",
|
|
28
|
+
"system_prompt",
|
|
29
|
+
"text",
|
|
30
|
+
"user_message",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
RAW_PAYLOAD_KEYS = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"api_key",
|
|
37
|
+
"credential",
|
|
38
|
+
"credentials",
|
|
39
|
+
"payload",
|
|
40
|
+
"provider_payload",
|
|
41
|
+
"provider_response",
|
|
42
|
+
"raw",
|
|
43
|
+
"raw_adk_object",
|
|
44
|
+
"raw_api_payload",
|
|
45
|
+
"raw_input",
|
|
46
|
+
"raw_output",
|
|
47
|
+
"raw_payload",
|
|
48
|
+
"raw_prompt",
|
|
49
|
+
"raw_provider_payload",
|
|
50
|
+
"raw_provider_response",
|
|
51
|
+
"raw_response",
|
|
52
|
+
"raw_tool_input",
|
|
53
|
+
"raw_tool_output",
|
|
54
|
+
"raw_user_message",
|
|
55
|
+
"secret",
|
|
56
|
+
"token",
|
|
57
|
+
"tool_context",
|
|
58
|
+
"tool_input",
|
|
59
|
+
"tool_output",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
SENSITIVE_KEY_EXCEPTIONS = frozenset({"raw_output_digest"})
|
|
64
|
+
|
|
65
|
+
REF_FIELDS = frozenset(
|
|
66
|
+
{
|
|
67
|
+
"evidence_refs",
|
|
68
|
+
"audit_refs",
|
|
69
|
+
"agent_advice_refs",
|
|
70
|
+
"tool_audit_refs",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
REF_ITEM_KEYS = frozenset({"ref", "kind", "purpose", "metadata"})
|
|
74
|
+
|
|
75
|
+
NO_EXECUTION_FIELDS = frozenset(
|
|
76
|
+
{
|
|
77
|
+
"execution_enabled",
|
|
78
|
+
"runtime_permission_granted",
|
|
79
|
+
"agent_runtime_enabled",
|
|
80
|
+
"llm_call_enabled",
|
|
81
|
+
"action_execution_enabled",
|
|
82
|
+
"chat_enabled",
|
|
83
|
+
"gateway_enabled",
|
|
84
|
+
"tool_execution_enabled",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
FORBIDDEN_OBJECT_MODULE_PREFIXES = (
|
|
89
|
+
"google.adk",
|
|
90
|
+
"adk_adapter",
|
|
91
|
+
"runtime_container",
|
|
92
|
+
"composition",
|
|
93
|
+
"litellm",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ProductGatewayResponseSummaryHeaderGuard:
|
|
98
|
+
"""Validate the frozen product gateway response summary header."""
|
|
99
|
+
|
|
100
|
+
guard_name = "product_gateway_response_summary_header_guard"
|
|
101
|
+
|
|
102
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
103
|
+
violations: list[str] = []
|
|
104
|
+
expected = {
|
|
105
|
+
"product": PRODUCT_GATEWAY_RESPONSE_SUMMARY_PRODUCT,
|
|
106
|
+
"payload_type": PRODUCT_GATEWAY_RESPONSE_SUMMARY_PAYLOAD_TYPE,
|
|
107
|
+
"payload_version": PRODUCT_GATEWAY_RESPONSE_SUMMARY_VERSION,
|
|
108
|
+
}
|
|
109
|
+
for key, expected_value in expected.items():
|
|
110
|
+
if summary.get(key) != expected_value:
|
|
111
|
+
violations.append(f"{key} must be {expected_value}.")
|
|
112
|
+
for key in ("request_id", "entry_kind", "status"):
|
|
113
|
+
if not isinstance(summary.get(key), str) or not summary.get(key):
|
|
114
|
+
violations.append(f"{key} is required.")
|
|
115
|
+
entry_kind = summary.get("entry_kind")
|
|
116
|
+
if isinstance(entry_kind, str) and (
|
|
117
|
+
entry_kind not in PRODUCT_GATEWAY_RESPONSE_SUMMARY_ENTRY_KINDS
|
|
118
|
+
):
|
|
119
|
+
violations.append(f"unsupported product_gateway entry_kind: {entry_kind}.")
|
|
120
|
+
status = summary.get("status")
|
|
121
|
+
if isinstance(status, str) and status not in PRODUCT_GATEWAY_RESPONSE_SUMMARY_STATUSES:
|
|
122
|
+
violations.append(f"unsupported product_gateway status: {status}.")
|
|
123
|
+
return _result(violations)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ProductGatewayResponseSummaryOnlyGuard:
|
|
127
|
+
"""Reject payloads that carry response bodies instead of summaries."""
|
|
128
|
+
|
|
129
|
+
guard_name = "product_gateway_response_summary_only_guard"
|
|
130
|
+
|
|
131
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
132
|
+
violations = [
|
|
133
|
+
f"summary-only field is forbidden at {path}."
|
|
134
|
+
for path, _value in _walk(summary)
|
|
135
|
+
if _key_at_path(path) in SUMMARY_ONLY_FORBIDDEN_KEYS
|
|
136
|
+
]
|
|
137
|
+
return _result(violations)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ProductGatewayResponseRefsOnlyGuard:
|
|
141
|
+
"""Validate that refs are sanitized ref items only."""
|
|
142
|
+
|
|
143
|
+
guard_name = "product_gateway_response_refs_only_guard"
|
|
144
|
+
|
|
145
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
146
|
+
violations: list[str] = []
|
|
147
|
+
for field_name in REF_FIELDS:
|
|
148
|
+
values = summary.get(field_name, [])
|
|
149
|
+
if not isinstance(values, (list, tuple)):
|
|
150
|
+
violations.append(f"{field_name} must be a list.")
|
|
151
|
+
continue
|
|
152
|
+
for index, value in enumerate(values):
|
|
153
|
+
item_path = f"$.{field_name}[{index}]"
|
|
154
|
+
if not isinstance(value, Mapping):
|
|
155
|
+
violations.append(f"{item_path} must be a mapping.")
|
|
156
|
+
continue
|
|
157
|
+
extra_keys = sorted(str(key) for key in set(value.keys()) - REF_ITEM_KEYS)
|
|
158
|
+
for key in extra_keys:
|
|
159
|
+
violations.append(f"{item_path}.{key} is not allowed in refs.")
|
|
160
|
+
if not isinstance(value.get("ref"), str) or not value.get("ref"):
|
|
161
|
+
violations.append(f"{item_path}.ref is required.")
|
|
162
|
+
if not isinstance(value.get("kind"), str) or not value.get("kind"):
|
|
163
|
+
violations.append(f"{item_path}.kind is required.")
|
|
164
|
+
metadata = value.get("metadata", {})
|
|
165
|
+
if metadata is not None and not isinstance(metadata, Mapping):
|
|
166
|
+
violations.append(f"{item_path}.metadata must be a mapping.")
|
|
167
|
+
return _result(violations)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ProductGatewayResponseNoRawPayloadGuard:
|
|
171
|
+
"""Reject raw or sensitive payload fields."""
|
|
172
|
+
|
|
173
|
+
guard_name = "product_gateway_response_no_raw_payload_guard"
|
|
174
|
+
|
|
175
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
176
|
+
violations = [
|
|
177
|
+
f"raw or sensitive payload is forbidden at {path}."
|
|
178
|
+
for path, value in _walk(summary)
|
|
179
|
+
if _is_raw_payload(path, value)
|
|
180
|
+
]
|
|
181
|
+
return _result(violations)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ProductGatewayResponseNoExecutionGuard:
|
|
185
|
+
"""Reject execution-enabling flags."""
|
|
186
|
+
|
|
187
|
+
guard_name = "product_gateway_response_no_execution_guard"
|
|
188
|
+
|
|
189
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
190
|
+
violations = [
|
|
191
|
+
f"{path} must not be true."
|
|
192
|
+
for path, value in _walk(summary)
|
|
193
|
+
if _key_at_path(path) in NO_EXECUTION_FIELDS and value is True
|
|
194
|
+
]
|
|
195
|
+
return _result(violations)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ProductGatewayResponseNoRuntimeObjectLeakageGuard:
|
|
199
|
+
"""Reject runtime, ADK, composition, and provider object markers."""
|
|
200
|
+
|
|
201
|
+
guard_name = "product_gateway_response_no_runtime_object_leakage_guard"
|
|
202
|
+
|
|
203
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
204
|
+
violations = [
|
|
205
|
+
f"runtime object leakage is forbidden at {path}."
|
|
206
|
+
for path, value in _walk(summary)
|
|
207
|
+
if _is_runtime_object(value)
|
|
208
|
+
]
|
|
209
|
+
return _result(violations)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ProductGatewayResponseBlockedRequiresReasonGuard:
|
|
213
|
+
"""Require blocked summaries to carry explicit blocking reasons."""
|
|
214
|
+
|
|
215
|
+
guard_name = "product_gateway_response_blocked_requires_reason_guard"
|
|
216
|
+
|
|
217
|
+
def validate(self, summary: Mapping[str, Any]) -> CandidateGuardResult:
|
|
218
|
+
if summary.get("status") != "blocked":
|
|
219
|
+
return _result([])
|
|
220
|
+
reasons = summary.get("blocking_reasons")
|
|
221
|
+
if isinstance(reasons, (list, tuple)) and any(
|
|
222
|
+
isinstance(reason, str) and reason for reason in reasons
|
|
223
|
+
):
|
|
224
|
+
return _result([])
|
|
225
|
+
return _result(["blocked product gateway summaries require blocking_reasons."])
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
DEFAULT_PRODUCT_GATEWAY_RESPONSE_SUMMARY_GUARDS = (
|
|
229
|
+
ProductGatewayResponseSummaryHeaderGuard(),
|
|
230
|
+
ProductGatewayResponseSummaryOnlyGuard(),
|
|
231
|
+
ProductGatewayResponseRefsOnlyGuard(),
|
|
232
|
+
ProductGatewayResponseNoRawPayloadGuard(),
|
|
233
|
+
ProductGatewayResponseNoExecutionGuard(),
|
|
234
|
+
ProductGatewayResponseNoRuntimeObjectLeakageGuard(),
|
|
235
|
+
ProductGatewayResponseBlockedRequiresReasonGuard(),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def validate_product_gateway_response_summary_guards(
|
|
240
|
+
summary: Mapping[str, Any],
|
|
241
|
+
guards: tuple[
|
|
242
|
+
ProductGatewayResponseSummaryHeaderGuard
|
|
243
|
+
| ProductGatewayResponseSummaryOnlyGuard
|
|
244
|
+
| ProductGatewayResponseRefsOnlyGuard
|
|
245
|
+
| ProductGatewayResponseNoRawPayloadGuard
|
|
246
|
+
| ProductGatewayResponseNoExecutionGuard
|
|
247
|
+
| ProductGatewayResponseNoRuntimeObjectLeakageGuard
|
|
248
|
+
| ProductGatewayResponseBlockedRequiresReasonGuard,
|
|
249
|
+
...,
|
|
250
|
+
] = DEFAULT_PRODUCT_GATEWAY_RESPONSE_SUMMARY_GUARDS,
|
|
251
|
+
) -> CandidateGuardResult:
|
|
252
|
+
"""Run product gateway response summary guards without executing anything."""
|
|
253
|
+
|
|
254
|
+
violations: list[str] = []
|
|
255
|
+
for guard in guards:
|
|
256
|
+
result = guard.validate(summary)
|
|
257
|
+
violations.extend(f"{guard.guard_name}: {item}" for item in result.violations)
|
|
258
|
+
return _result(violations)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _result(violations: list[str]) -> CandidateGuardResult:
|
|
262
|
+
return CandidateGuardResult(
|
|
263
|
+
passed=not violations,
|
|
264
|
+
violations=tuple(violations),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _walk(value: Any, path: str = "$") -> list[tuple[str, Any]]:
|
|
269
|
+
items = [(path, value)]
|
|
270
|
+
if isinstance(value, Mapping):
|
|
271
|
+
for key, item in value.items():
|
|
272
|
+
items.extend(_walk(item, f"{path}.{key}"))
|
|
273
|
+
elif isinstance(value, (list, tuple)):
|
|
274
|
+
for index, item in enumerate(value):
|
|
275
|
+
items.extend(_walk(item, f"{path}[{index}]"))
|
|
276
|
+
return items
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _key_at_path(path: str) -> str:
|
|
280
|
+
return path.rsplit(".", maxsplit=1)[-1].split("[", maxsplit=1)[0].lower()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _is_raw_payload(path: str, value: Any) -> bool:
|
|
284
|
+
key = _key_at_path(path)
|
|
285
|
+
if key in SENSITIVE_KEY_EXCEPTIONS:
|
|
286
|
+
return False
|
|
287
|
+
if key in RAW_PAYLOAD_KEYS or key.endswith("_token") or key.endswith("_secret"):
|
|
288
|
+
return True
|
|
289
|
+
if isinstance(value, str):
|
|
290
|
+
lowered = value.lower()
|
|
291
|
+
return any(
|
|
292
|
+
marker in lowered
|
|
293
|
+
for marker in (
|
|
294
|
+
"raw provider response",
|
|
295
|
+
"raw_response",
|
|
296
|
+
"response_text",
|
|
297
|
+
"system_prompt",
|
|
298
|
+
"raw_tool_input",
|
|
299
|
+
"raw_tool_output",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_runtime_object(value: Any) -> bool:
|
|
306
|
+
if isinstance(value, Mapping):
|
|
307
|
+
module_name = value.get("object_module")
|
|
308
|
+
return isinstance(module_name, str) and module_name.startswith(
|
|
309
|
+
FORBIDDEN_OBJECT_MODULE_PREFIXES
|
|
310
|
+
)
|
|
311
|
+
if value is None or isinstance(value, (str, int, float, bool, list, tuple, dict)):
|
|
312
|
+
return False
|
|
313
|
+
return type(value).__module__.startswith(FORBIDDEN_OBJECT_MODULE_PREFIXES)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
__all__ = [
|
|
317
|
+
"DEFAULT_PRODUCT_GATEWAY_RESPONSE_SUMMARY_GUARDS",
|
|
318
|
+
"ProductGatewayResponseBlockedRequiresReasonGuard",
|
|
319
|
+
"ProductGatewayResponseNoExecutionGuard",
|
|
320
|
+
"ProductGatewayResponseNoRawPayloadGuard",
|
|
321
|
+
"ProductGatewayResponseNoRuntimeObjectLeakageGuard",
|
|
322
|
+
"ProductGatewayResponseRefsOnlyGuard",
|
|
323
|
+
"ProductGatewayResponseSummaryHeaderGuard",
|
|
324
|
+
"ProductGatewayResponseSummaryOnlyGuard",
|
|
325
|
+
"validate_product_gateway_response_summary_guards",
|
|
326
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Runtime-facing behavior contracts for Cognition Engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from schemas.runtime import (
|
|
8
|
+
AdkServiceFactsSummaryInput,
|
|
9
|
+
ArtifactDelta,
|
|
10
|
+
NodeExecutionInput,
|
|
11
|
+
NodeExecutionResult,
|
|
12
|
+
RecordedRunEvidenceInput,
|
|
13
|
+
ResumePoint,
|
|
14
|
+
RuntimeEvent,
|
|
15
|
+
RuntimeInput,
|
|
16
|
+
RuntimeResult,
|
|
17
|
+
StateDelta,
|
|
18
|
+
WorkflowInput,
|
|
19
|
+
WorkflowResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RuntimeRunner(Protocol):
|
|
24
|
+
"""Contract for executing one runtime task."""
|
|
25
|
+
|
|
26
|
+
def run(self, runtime_input: RuntimeInput) -> RuntimeResult:
|
|
27
|
+
"""Execute a runtime task."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkflowRunner(Protocol):
|
|
31
|
+
"""Contract for executing a workflow."""
|
|
32
|
+
|
|
33
|
+
def run_workflow(self, workflow_input: WorkflowInput) -> WorkflowResult:
|
|
34
|
+
"""Execute a workflow."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NodeRunner(Protocol):
|
|
38
|
+
"""Contract for executing one node."""
|
|
39
|
+
|
|
40
|
+
def run_node(self, node_input: NodeExecutionInput) -> NodeExecutionResult:
|
|
41
|
+
"""Execute a node."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NodeScheduler(Protocol):
|
|
45
|
+
"""Contract for scheduling node execution."""
|
|
46
|
+
|
|
47
|
+
def schedule_nodes(
|
|
48
|
+
self,
|
|
49
|
+
workflow_input: WorkflowInput,
|
|
50
|
+
) -> list[NodeExecutionInput]:
|
|
51
|
+
"""Schedule nodes for a workflow input."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResumeController(Protocol):
|
|
55
|
+
"""Contract for resuming runtime execution."""
|
|
56
|
+
|
|
57
|
+
def resume(self, resume_point: ResumePoint) -> RuntimeResult:
|
|
58
|
+
"""Resume execution from a resume point."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RuntimeEventPublisher(Protocol):
|
|
62
|
+
"""Contract for publishing runtime events."""
|
|
63
|
+
|
|
64
|
+
def publish_event(self, event: RuntimeEvent) -> None:
|
|
65
|
+
"""Publish a runtime event."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RuntimeArtifactPublisher(Protocol):
|
|
69
|
+
"""Contract for publishing runtime artifact deltas."""
|
|
70
|
+
|
|
71
|
+
def publish_artifact_delta(self, artifact_delta: ArtifactDelta) -> None:
|
|
72
|
+
"""Publish an artifact delta."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RuntimeStatePublisher(Protocol):
|
|
76
|
+
"""Contract for publishing runtime state deltas."""
|
|
77
|
+
|
|
78
|
+
def publish_state_delta(self, state_delta: StateDelta) -> None:
|
|
79
|
+
"""Publish a state delta."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InvocationTracker(Protocol):
|
|
83
|
+
"""Contract for tracking invocation identity."""
|
|
84
|
+
|
|
85
|
+
def next_invocation_id(self) -> str:
|
|
86
|
+
"""Return the next invocation id."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RecordedRunEvidenceProvider(Protocol):
|
|
90
|
+
"""Contract for converting recorded runtime facts into recorded-run evidence."""
|
|
91
|
+
|
|
92
|
+
def build_recorded_run_evidence(
|
|
93
|
+
self,
|
|
94
|
+
runtime_result: RuntimeResult,
|
|
95
|
+
) -> RecordedRunEvidenceInput:
|
|
96
|
+
"""Build recorded-run evidence facts from a runtime result."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AdkServiceFactsProvider(Protocol):
|
|
100
|
+
"""Contract for converting recorded runtime facts into ADK service facts."""
|
|
101
|
+
|
|
102
|
+
def build_adk_service_facts(
|
|
103
|
+
self,
|
|
104
|
+
runtime_result: RuntimeResult,
|
|
105
|
+
) -> AdkServiceFactsSummaryInput:
|
|
106
|
+
"""Build ADK service facts from a runtime result."""
|