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.
@@ -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."""
@@ -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."""