sondera-harness 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. sondera/__init__.py +111 -0
  2. sondera/__main__.py +4 -0
  3. sondera/adk/__init__.py +3 -0
  4. sondera/adk/analyze.py +222 -0
  5. sondera/adk/plugin.py +387 -0
  6. sondera/cli.py +22 -0
  7. sondera/exceptions.py +167 -0
  8. sondera/harness/__init__.py +6 -0
  9. sondera/harness/abc.py +102 -0
  10. sondera/harness/cedar/__init__.py +0 -0
  11. sondera/harness/cedar/harness.py +363 -0
  12. sondera/harness/cedar/schema.py +225 -0
  13. sondera/harness/sondera/__init__.py +0 -0
  14. sondera/harness/sondera/_grpc.py +354 -0
  15. sondera/harness/sondera/harness.py +890 -0
  16. sondera/langgraph/__init__.py +15 -0
  17. sondera/langgraph/analyze.py +543 -0
  18. sondera/langgraph/exceptions.py +19 -0
  19. sondera/langgraph/graph.py +210 -0
  20. sondera/langgraph/middleware.py +454 -0
  21. sondera/proto/google/protobuf/any_pb2.py +37 -0
  22. sondera/proto/google/protobuf/any_pb2.pyi +14 -0
  23. sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
  24. sondera/proto/google/protobuf/duration_pb2.py +37 -0
  25. sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
  26. sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
  27. sondera/proto/google/protobuf/empty_pb2.py +37 -0
  28. sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
  29. sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
  30. sondera/proto/google/protobuf/struct_pb2.py +47 -0
  31. sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
  32. sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
  33. sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
  34. sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
  35. sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
  36. sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
  37. sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
  38. sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
  39. sondera/proto/sondera/__init__.py +0 -0
  40. sondera/proto/sondera/core/__init__.py +0 -0
  41. sondera/proto/sondera/core/v1/__init__.py +0 -0
  42. sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
  43. sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
  44. sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
  45. sondera/proto/sondera/harness/__init__.py +0 -0
  46. sondera/proto/sondera/harness/v1/__init__.py +0 -0
  47. sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
  48. sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
  49. sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
  50. sondera/py.typed +0 -0
  51. sondera/settings.py +20 -0
  52. sondera/strands/__init__.py +5 -0
  53. sondera/strands/analyze.py +244 -0
  54. sondera/strands/harness.py +333 -0
  55. sondera/tui/__init__.py +0 -0
  56. sondera/tui/app.py +309 -0
  57. sondera/tui/screens/__init__.py +5 -0
  58. sondera/tui/screens/adjudication.py +184 -0
  59. sondera/tui/screens/agent.py +158 -0
  60. sondera/tui/screens/trajectory.py +158 -0
  61. sondera/tui/widgets/__init__.py +23 -0
  62. sondera/tui/widgets/agent_card.py +94 -0
  63. sondera/tui/widgets/agent_list.py +73 -0
  64. sondera/tui/widgets/recent_adjudications.py +52 -0
  65. sondera/tui/widgets/recent_trajectories.py +54 -0
  66. sondera/tui/widgets/summary.py +57 -0
  67. sondera/tui/widgets/tool_card.py +33 -0
  68. sondera/tui/widgets/violation_panel.py +72 -0
  69. sondera/tui/widgets/violations_list.py +78 -0
  70. sondera/tui/widgets/violations_summary.py +104 -0
  71. sondera/types.py +346 -0
  72. sondera_harness-0.6.0.dist-info/METADATA +323 -0
  73. sondera_harness-0.6.0.dist-info/RECORD +77 -0
  74. sondera_harness-0.6.0.dist-info/WHEEL +5 -0
  75. sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
  76. sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
  77. sondera_harness-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,363 @@
1
+ """
2
+ CedarPolicyEngine local harness implementation.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import uuid
8
+
9
+ from cedar.schema import CedarSchema
10
+
11
+ from cedar import (
12
+ Authorizer,
13
+ Context,
14
+ Entity,
15
+ EntityUid,
16
+ PolicySet,
17
+ Request,
18
+ Schema,
19
+ )
20
+ from sondera.harness.abc import Harness as AbstractHarness
21
+ from sondera.types import (
22
+ Adjudication,
23
+ Agent,
24
+ Content,
25
+ Decision,
26
+ PromptContent,
27
+ Role,
28
+ Stage,
29
+ ToolRequestContent,
30
+ ToolResponseContent,
31
+ )
32
+
33
+ _LOGGER = logging.getLogger(__name__)
34
+
35
+
36
+ class CedarPolicyHarness(AbstractHarness):
37
+ """CedarPolicyHarness is a local policy-as-code harness for Agent Scaffolds.
38
+
39
+ Uses Cedar policy language to evaluate tool invocations against a policy set.
40
+ The schema is generated from the agent's tools using schema.py, with each tool
41
+ becoming a Cedar action with typed parameters and response context.
42
+
43
+ Actions:
44
+ - Each tool becomes an action (e.g., MyAgent::Action::"my_tool")
45
+ - PRE_TOOL stage evaluates with 'parameters' context
46
+ - POST_TOOL stage evaluates with 'response' context
47
+
48
+ Example policy to allow all tool invocations for an agent named "MyAgent":
49
+ permit(principal, action, resource)
50
+ when { principal is MyAgent::Agent };
51
+
52
+ Example policy to deny a specific tool:
53
+ forbid(
54
+ principal,
55
+ action == MyAgent::Action::"dangerous_tool",
56
+ resource
57
+ );
58
+
59
+ Example policy with parameter constraints:
60
+ forbid(
61
+ principal,
62
+ action == MyAgent::Action::"bash",
63
+ resource
64
+ ) when { context.parameters.command like "*rm -rf*" };
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ *,
70
+ policy_set: PolicySet | str,
71
+ schema: CedarSchema,
72
+ logger: logging.Logger | None = None,
73
+ ):
74
+ """Initialize the Cedar policy engine.
75
+
76
+ Args:
77
+ policy_set: Cedar policies to evaluate. Can be a PolicySet instance
78
+ or Cedar policy text. Required.
79
+ schema: Cedar schema generated from agent_to_cedar_schema(). Required.
80
+ logger: Logger instance.
81
+
82
+ Raises:
83
+ ValueError: If policy_set or schema is not provided.
84
+ """
85
+ self._agent: Agent | None = None
86
+ self._trajectory_id: str | None = None
87
+ self._trajectory_step_count: int = 0
88
+ self._logger = logger or _LOGGER
89
+
90
+ if schema is None:
91
+ raise ValueError("schema is required")
92
+ if policy_set is None:
93
+ raise ValueError("policy_set is required")
94
+
95
+ self._cedar_schema = schema
96
+ # Exclude None values when serializing to JSON for Cedar compatibility
97
+ self._schema = Schema.from_json(schema.model_dump_json(exclude_none=True))
98
+
99
+ # Parse policy set
100
+ if isinstance(policy_set, str):
101
+ self._policy_set = PolicySet(policy_set)
102
+ else:
103
+ self._policy_set = policy_set
104
+ # Extract namespace name from schema
105
+ namespaces = list(schema.root.keys())
106
+ if namespaces:
107
+ # The schema has a single namespace keyed by name
108
+ self._namespace = namespaces[0]
109
+ else:
110
+ raise ValueError("Schema must have at least one namespace")
111
+ # Authorizer will be initialized with entities when agent is set
112
+ self._authorizer: Authorizer | None = None
113
+
114
+ def _build_authorizer(self) -> Authorizer:
115
+ """Build the Cedar authorizer with current entities."""
116
+ if not self._trajectory_id:
117
+ raise RuntimeError("_build_authorizer called without trajectory_id")
118
+
119
+ entities: list[Entity] = [
120
+ Entity(
121
+ EntityUid(f"{self._namespace}::Trajectory", self._trajectory_id),
122
+ {
123
+ "step_count": self._trajectory_step_count,
124
+ },
125
+ )
126
+ ]
127
+
128
+ if self._agent:
129
+ agent_uid = EntityUid(f"{self._namespace}::Agent", self._agent.id)
130
+
131
+ # Add tool entities from agent's tools
132
+ tool_entities: list[EntityUid] = []
133
+ for tool in self._agent.tools:
134
+ tool_id = tool.id or tool.name
135
+ tool_uid = EntityUid(f"{self._namespace}::Tool", tool_id)
136
+ tool_entity = Entity(
137
+ tool_uid,
138
+ {
139
+ "name": tool.name,
140
+ "description": tool.description,
141
+ },
142
+ )
143
+ tool_entities.append(tool_uid)
144
+ entities.append(tool_entity)
145
+
146
+ agent_entity = Entity(
147
+ agent_uid,
148
+ {
149
+ "name": self._agent.name,
150
+ "provider_id": self._agent.provider_id,
151
+ "tools": tool_entities,
152
+ },
153
+ )
154
+ entities.append(agent_entity)
155
+
156
+ return Authorizer(entities=entities, schema=self._schema)
157
+
158
+ async def resume(
159
+ self,
160
+ trajectory_id: str,
161
+ *,
162
+ agent: Agent | None = None,
163
+ ) -> None:
164
+ """Resume an existing trajectory.
165
+
166
+ Args:
167
+ trajectory_id: The trajectory ID to resume.
168
+ agent: Optional agent to use (overrides constructor agent).
169
+ """
170
+ raise NotImplementedError(
171
+ "Resuming trajectories is not supported in CedarPolicyHarness."
172
+ )
173
+
174
+ async def initialize(
175
+ self,
176
+ *,
177
+ agent: Agent | None = None,
178
+ ) -> None:
179
+ """Initialize a new trajectory.
180
+
181
+ Args:
182
+ agent: Optional agent to use (overrides constructor agent).
183
+ """
184
+ if agent:
185
+ self._agent = agent
186
+ self._trajectory_id = f"traj-{uuid.uuid4()}"
187
+ self._trajectory_step_count = 0
188
+ self._authorizer = self._build_authorizer()
189
+ self._logger.debug("Initialized trajectory %s", self._trajectory_id)
190
+
191
+ async def finalize(self) -> None:
192
+ """Finalize the current trajectory."""
193
+ if not self._trajectory_id:
194
+ raise ValueError("No active trajectory. Call initialize first.")
195
+ self._logger.debug("Finalized trajectory %s", self._trajectory_id)
196
+ self._trajectory_id = None
197
+ self._trajectory_step_count = 0
198
+
199
+ async def adjudicate(
200
+ self,
201
+ stage: Stage,
202
+ role: Role,
203
+ content: Content,
204
+ ) -> Adjudication:
205
+ """Adjudicate a trajectory step using Cedar policies.
206
+
207
+ Evaluates Cedar policies based on content type and stage:
208
+ - PRE_TOOL + ToolRequestContent: Evaluates tool action with 'parameters' context
209
+ - POST_TOOL + ToolResponseContent: Evaluates tool action with 'response' context
210
+ - Other content types: Allowed by default
211
+
212
+ The action name matches the tool name (sanitized for Cedar), and context
213
+ contains typed parameters/response based on the tool's JSON schema.
214
+
215
+ Args:
216
+ stage: The stage of the step.
217
+ role: The role of the step.
218
+ content: The content of the step.
219
+
220
+ Returns:
221
+ The adjudication result (ALLOW or DENY).
222
+ """
223
+ if not self._agent or not self._trajectory_id or not self._authorizer:
224
+ raise RuntimeError("initialize() must be called before adjudicate().")
225
+
226
+ # Build common entity UIDs using the schema's namespace
227
+ agent_uid = EntityUid(f"{self._namespace}::Agent", self._agent.id)
228
+ trajectory_uid = EntityUid(
229
+ f"{self._namespace}::Trajectory", self._trajectory_id
230
+ )
231
+ self._trajectory_step_count += 1
232
+ trajectory_entity = Entity(
233
+ trajectory_uid, {"step_count": self._trajectory_step_count}
234
+ )
235
+ self._authorizer.upsert_entity(trajectory_entity)
236
+
237
+ request: Request | None = None
238
+ match (stage, content):
239
+ case (Stage.PRE_MODEL | Stage.POST_MODEL, PromptContent()):
240
+ request = self._message_request(
241
+ agent_uid, trajectory_uid, role, content
242
+ )
243
+ case (Stage.PRE_TOOL, ToolRequestContent()):
244
+ request = self._tool_request(agent_uid, trajectory_uid, content)
245
+ case (Stage.POST_TOOL, ToolResponseContent()):
246
+ request = self._tool_response(agent_uid, trajectory_uid, content)
247
+ case _:
248
+ return Adjudication(
249
+ decision=Decision.ALLOW,
250
+ reason="Non-tool content allowed by default",
251
+ )
252
+ assert request is not None, "Unexpected none request"
253
+ response = self._authorizer.is_authorized(request, self._policy_set)
254
+ if str(response.decision) == "Allow":
255
+ reason = f"Allowed by policies: {response.reason}"
256
+ return Adjudication(decision=Decision.ALLOW, reason=reason)
257
+ else:
258
+ reason = f"Denied by policies: {response.reason}"
259
+ return Adjudication(decision=Decision.DENY, reason=reason)
260
+
261
+ def _message_request(
262
+ self,
263
+ agent_uid: EntityUid,
264
+ trajectory_uid: EntityUid,
265
+ role: Role,
266
+ content: PromptContent,
267
+ ) -> Request:
268
+ """Create a request for a message request against Cedar policies.
269
+
270
+ Args:
271
+ content: The tool response content.
272
+
273
+ Returns:
274
+ The request.
275
+ """
276
+ if not self._authorizer:
277
+ raise RuntimeError("_message_request called without authorizer")
278
+
279
+ role_euid = EntityUid(f"{self._namespace}::Role", role.value.lower())
280
+ message = Entity(
281
+ EntityUid(f"{self._namespace}::Message", str(uuid.uuid4())),
282
+ {"content": content.text, "role": role_euid},
283
+ [trajectory_uid],
284
+ )
285
+ self._authorizer.add_entity(message)
286
+ action_uid = EntityUid(f"{self._namespace}::Action", "Prompt")
287
+ return Request(
288
+ principal=agent_uid,
289
+ action=action_uid,
290
+ resource=message.uid(),
291
+ schema=self._schema,
292
+ )
293
+
294
+ def _tool_request(
295
+ self,
296
+ agent_uid: EntityUid,
297
+ trajectory_uid: EntityUid,
298
+ content: ToolRequestContent,
299
+ ) -> Request:
300
+ """Create a request for a tool request against Cedar policies.
301
+
302
+ Args:
303
+ content: The tool response content.
304
+
305
+ Returns:
306
+ The request.
307
+ """
308
+ # Build entity UIDs using the schema's namespace
309
+ tool_id = content.tool_id
310
+ # Sanitize tool_id to match action name generation in schema.py
311
+ action_name = tool_id.replace(" ", "_").replace("-", "_")
312
+ action_uid = EntityUid(f"{self._namespace}::Action", action_name)
313
+
314
+ context = Context(
315
+ {"parameters_json": json.dumps(content.args), "parameters": content.args},
316
+ schema=self._schema,
317
+ action=action_uid,
318
+ )
319
+
320
+ return Request(
321
+ principal=agent_uid,
322
+ action=action_uid,
323
+ resource=trajectory_uid,
324
+ context=context,
325
+ schema=self._schema,
326
+ )
327
+
328
+ def _tool_response(
329
+ self,
330
+ agent_uid: EntityUid,
331
+ trajectory_uid: EntityUid,
332
+ content: ToolResponseContent,
333
+ ) -> Request:
334
+ """Create a request for a tool response against Cedar policies.
335
+
336
+ Args:
337
+ content: The tool response content.
338
+
339
+ Returns:
340
+ The request.
341
+ """
342
+ # Build entity UIDs using the schema's namespace
343
+ tool_id = content.tool_id
344
+ # Sanitize tool_id to match action name generation in schema.py
345
+ action_name = tool_id.replace(" ", "_").replace("-", "_")
346
+ action_uid = EntityUid(f"{self._namespace}::Action", action_name)
347
+
348
+ context = Context(
349
+ {
350
+ "response_json": json.dumps(content.response, default=str),
351
+ "response": content.response,
352
+ },
353
+ schema=self._schema,
354
+ action=action_uid,
355
+ )
356
+
357
+ return Request(
358
+ principal=agent_uid,
359
+ action=action_uid,
360
+ resource=trajectory_uid,
361
+ context=context,
362
+ schema=self._schema,
363
+ )
@@ -0,0 +1,225 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ from cedar.schema import (
5
+ Action,
6
+ AppliesTo,
7
+ CedarSchema,
8
+ EntityType,
9
+ NamespaceDefinition,
10
+ SchemaType,
11
+ validate,
12
+ )
13
+
14
+ from sondera.types import Agent, Tool
15
+
16
+
17
+ def openai_json_schema_to_cedar_type(json_schema_str: str | None) -> SchemaType | None:
18
+ """Convert an OpenAI JSON schema string to Cedar SchemaType.
19
+
20
+ Args:
21
+ json_schema_str: JSON schema string in OpenAI format
22
+
23
+ Returns:
24
+ Cedar SchemaType or None if input is None/empty
25
+ """
26
+ if not json_schema_str:
27
+ return None
28
+ try:
29
+ schema = json.loads(json_schema_str)
30
+ except json.JSONDecodeError as e:
31
+ raise e
32
+ return json_schema_to_cedar_type(schema)
33
+
34
+
35
+ def json_schema_to_cedar_type(schema: dict[str, Any]) -> SchemaType:
36
+ """Convert a JSON schema object to Cedar SchemaType.
37
+
38
+ Maps JSON Schema types to Cedar types:
39
+ - object/OBJECT -> Record with attributes
40
+ - array/ARRAY -> Set with element type
41
+ - string/STRING -> String
42
+ - number/integer/NUMBER/INTEGER -> Long
43
+ - boolean/BOOLEAN -> Boolean
44
+ """
45
+ if not isinstance(schema, dict):
46
+ return SchemaType(type="String") # Default fallback
47
+
48
+ # Handle both lowercase and uppercase type names
49
+ json_type = schema.get("type", "object").lower()
50
+
51
+ if json_type == "object":
52
+ properties = schema.get("properties", {})
53
+ required_fields = set(schema.get("required", []))
54
+
55
+ attributes = {}
56
+ for prop_name, prop_schema in properties.items():
57
+ cedar_type = json_schema_to_cedar_type(prop_schema)
58
+ # Only set required=False if the field is optional
59
+ # Cedar defaults to required=true, so we don't need to set it explicitly
60
+ if prop_name not in required_fields:
61
+ cedar_type.required = False
62
+ attributes[prop_name] = cedar_type
63
+
64
+ return SchemaType(type="Record", attributes=attributes)
65
+
66
+ elif json_type == "array":
67
+ items = schema.get("items", {})
68
+ element_type = json_schema_to_cedar_type(items)
69
+ return SchemaType(type="Set", element=element_type)
70
+
71
+ elif json_type == "string":
72
+ # Check for enum values which could be treated as specific strings
73
+ if "enum" in schema:
74
+ # For now, just treat as String
75
+ return SchemaType(type="String")
76
+ return SchemaType(type="String")
77
+
78
+ elif json_type in ["number", "integer"]:
79
+ return SchemaType(type="Long")
80
+
81
+ elif json_type == "boolean":
82
+ return SchemaType(type="Boolean")
83
+
84
+ else:
85
+ # Default to String for unknown types
86
+ return SchemaType(type="String")
87
+
88
+
89
+ def tool_to_action(tool: Tool) -> Action:
90
+ """Convert a Tool to a Cedar Action.
91
+
92
+ Creates an Action with context containing both parameters and response.
93
+ Both are always included and marked as optional since they may not be
94
+ present in all requests (parameters for PRE_TOOL, response for POST_TOOL).
95
+
96
+ If a typed schema is available, it's used. Otherwise, a JSON string fallback
97
+ is provided for flexibility (parameters_json/response_json).
98
+ """
99
+ context_attributes: dict[str, SchemaType] = {}
100
+
101
+ # Add parameters to context - use typed schema if available
102
+ if tool.parameters_json_schema:
103
+ params_type = openai_json_schema_to_cedar_type(tool.parameters_json_schema)
104
+ if params_type and params_type.type == "Record" and params_type.attributes:
105
+ # Use the parameters directly as a Record type, mark as optional
106
+ params_type.required = False
107
+ context_attributes["parameters"] = params_type
108
+ elif params_type:
109
+ # Wrap non-record parameters
110
+ wrapped_type = SchemaType(
111
+ type="Record", attributes={"value": params_type}, required=False
112
+ )
113
+ context_attributes["parameters"] = wrapped_type
114
+
115
+ # Always add parameters_json as a string fallback
116
+ context_attributes["parameters_json"] = SchemaType(type="String", required=False)
117
+
118
+ # Add response to context - use typed schema if available
119
+ if tool.response_json_schema:
120
+ response_type = openai_json_schema_to_cedar_type(tool.response_json_schema)
121
+ if (
122
+ response_type
123
+ and response_type.type == "Record"
124
+ and response_type.attributes
125
+ ):
126
+ # Use the response directly as a Record type, mark as optional
127
+ response_type.required = False
128
+ context_attributes["response"] = response_type
129
+ elif response_type:
130
+ # For simple types, wrap in a Record
131
+ wrapped_type = SchemaType(
132
+ type="Record", attributes={"value": response_type}, required=False
133
+ )
134
+ context_attributes["response"] = wrapped_type
135
+
136
+ # Always add response_json as a string fallback
137
+ context_attributes["response_json"] = SchemaType(type="String", required=False)
138
+
139
+ # Create context with both typed and string representations
140
+ context = SchemaType(type="Record", attributes=context_attributes)
141
+
142
+ # Create the action with appliesTo configuration
143
+ # Default to Agent as principal and Tool as resource
144
+ action = Action(
145
+ appliesTo=AppliesTo(
146
+ principalTypes=["Agent"], resourceTypes=["Trajectory"], context=context
147
+ )
148
+ )
149
+
150
+ return action
151
+
152
+
153
+ def agent_to_cedar_schema(agent: Agent) -> CedarSchema:
154
+ """Convert an Agent to a Cedar Schema.
155
+
156
+ Creates a namespace named after the agent containing:
157
+ - Default entity types (Agent, Tool)
158
+ - Actions for each tool with parameters/response in context
159
+ """
160
+
161
+ # Create entity types
162
+ entity_types: dict[str, EntityType] = {
163
+ "Agent": EntityType(
164
+ shape=SchemaType(
165
+ type="Record",
166
+ attributes={
167
+ "name": SchemaType(type="String"),
168
+ "provider_id": SchemaType(type="String"),
169
+ "tools": SchemaType(
170
+ type="Set", element=SchemaType(name="Tool", type="Entity")
171
+ ),
172
+ },
173
+ )
174
+ ),
175
+ "Tool": EntityType(
176
+ shape=SchemaType(
177
+ type="Record",
178
+ attributes={
179
+ "name": SchemaType(type="String"),
180
+ "description": SchemaType(type="String"),
181
+ },
182
+ )
183
+ ),
184
+ "Role": EntityType(enum=["user", "model", "system", "tool"]),
185
+ "Message": EntityType(
186
+ shape=SchemaType(
187
+ type="Record",
188
+ attributes={
189
+ "content": SchemaType(type="String"),
190
+ "role": SchemaType(name="Role", type="Entity"),
191
+ },
192
+ ),
193
+ memberOfTypes=["Trajectory"],
194
+ ),
195
+ "Trajectory": EntityType(
196
+ shape=SchemaType(
197
+ type="Record",
198
+ attributes={
199
+ "step_count": SchemaType(type="Long"),
200
+ },
201
+ )
202
+ ),
203
+ }
204
+
205
+ # Create actions from lean
206
+ actions: dict[str, Action] = {}
207
+ for tool in agent.tools:
208
+ # Use tool name as action name, sanitized for Cedar
209
+ action_name = tool.name.replace(" ", "_").replace("-", "_")
210
+ actions[action_name] = tool_to_action(tool)
211
+
212
+ actions["Prompt"] = Action(
213
+ appliesTo=AppliesTo(principalTypes=["Agent"], resourceTypes=["Message"])
214
+ )
215
+
216
+ # Create namespace definition
217
+ namespace_name = agent.name.replace(" ", "_").replace("-", "_")
218
+ namespace_def = NamespaceDefinition(entityTypes=entity_types, actions=actions)
219
+
220
+ # Create the schema with the namespace
221
+ schema = CedarSchema(root={namespace_name: namespace_def})
222
+
223
+ validate(schema)
224
+
225
+ return schema
File without changes