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.
- sondera/__init__.py +111 -0
- sondera/__main__.py +4 -0
- sondera/adk/__init__.py +3 -0
- sondera/adk/analyze.py +222 -0
- sondera/adk/plugin.py +387 -0
- sondera/cli.py +22 -0
- sondera/exceptions.py +167 -0
- sondera/harness/__init__.py +6 -0
- sondera/harness/abc.py +102 -0
- sondera/harness/cedar/__init__.py +0 -0
- sondera/harness/cedar/harness.py +363 -0
- sondera/harness/cedar/schema.py +225 -0
- sondera/harness/sondera/__init__.py +0 -0
- sondera/harness/sondera/_grpc.py +354 -0
- sondera/harness/sondera/harness.py +890 -0
- sondera/langgraph/__init__.py +15 -0
- sondera/langgraph/analyze.py +543 -0
- sondera/langgraph/exceptions.py +19 -0
- sondera/langgraph/graph.py +210 -0
- sondera/langgraph/middleware.py +454 -0
- sondera/proto/google/protobuf/any_pb2.py +37 -0
- sondera/proto/google/protobuf/any_pb2.pyi +14 -0
- sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/duration_pb2.py +37 -0
- sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
- sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/empty_pb2.py +37 -0
- sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
- sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/struct_pb2.py +47 -0
- sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
- sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
- sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
- sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
- sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
- sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
- sondera/proto/sondera/__init__.py +0 -0
- sondera/proto/sondera/core/__init__.py +0 -0
- sondera/proto/sondera/core/v1/__init__.py +0 -0
- sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
- sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
- sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
- sondera/proto/sondera/harness/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
- sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
- sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
- sondera/py.typed +0 -0
- sondera/settings.py +20 -0
- sondera/strands/__init__.py +5 -0
- sondera/strands/analyze.py +244 -0
- sondera/strands/harness.py +333 -0
- sondera/tui/__init__.py +0 -0
- sondera/tui/app.py +309 -0
- sondera/tui/screens/__init__.py +5 -0
- sondera/tui/screens/adjudication.py +184 -0
- sondera/tui/screens/agent.py +158 -0
- sondera/tui/screens/trajectory.py +158 -0
- sondera/tui/widgets/__init__.py +23 -0
- sondera/tui/widgets/agent_card.py +94 -0
- sondera/tui/widgets/agent_list.py +73 -0
- sondera/tui/widgets/recent_adjudications.py +52 -0
- sondera/tui/widgets/recent_trajectories.py +54 -0
- sondera/tui/widgets/summary.py +57 -0
- sondera/tui/widgets/tool_card.py +33 -0
- sondera/tui/widgets/violation_panel.py +72 -0
- sondera/tui/widgets/violations_list.py +78 -0
- sondera/tui/widgets/violations_summary.py +104 -0
- sondera/types.py +346 -0
- sondera_harness-0.6.0.dist-info/METADATA +323 -0
- sondera_harness-0.6.0.dist-info/RECORD +77 -0
- sondera_harness-0.6.0.dist-info/WHEEL +5 -0
- sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
- sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
- 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
|