Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aeabcccf6f | |||
| 6e81fe6a08 | |||
| 8b35066b0c | |||
| 34c0563f15 | |||
| ac5117aa05 |
+39
-14
@@ -209,14 +209,15 @@ class PolicyResult:
|
||||
accumulated and intends to apply on this decision
|
||||
(filtering already done). ``None`` when the policy
|
||||
wrote no labels, e.g. ``{"integrity": "0"}``.
|
||||
:param deciding_policy: Name of the policy whose action
|
||||
drove the composed result. Engine-set only —
|
||||
single-policy results leave it ``None``. On DENY: the
|
||||
first short-circuiting policy. On ASK: the first
|
||||
ASKing policy in YAML order. On ALLOW: ``None``.
|
||||
Powers the ``deciding_policy`` outer-span attribute
|
||||
(POLICIES.md §11.5) and the per-policy ``ask_timeout``
|
||||
lookup (§7.2).
|
||||
:param deciding_policies: Names of all policies that drove
|
||||
the composed result. Engine-set only — single-policy
|
||||
results leave it ``None``. On DENY: a single-element
|
||||
list with the short-circuiting policy. On ASK: all
|
||||
ASKing policies in YAML order. On ALLOW: ``None``.
|
||||
``deciding_policy`` is a computed property returning
|
||||
``deciding_policies[0]`` (or ``None`` when unset);
|
||||
all existing callers that read ``.deciding_policy``
|
||||
work unchanged.
|
||||
:param data: Optional replacement payload returned by the
|
||||
policy callable. When present on an ALLOW result, the
|
||||
enforcement site substitutes this value for the original
|
||||
@@ -225,6 +226,10 @@ class PolicyResult:
|
||||
phase). ``None`` means "use original content unchanged".
|
||||
``Any`` because the shape varies by phase: a dict of
|
||||
tool arguments on TOOL_CALL, a string on TOOL_RESULT.
|
||||
When multiple policies transform data, each policy
|
||||
receives the previous policy's output as its input —
|
||||
the engine feeds the composed result back as
|
||||
``ctx.content`` before dispatching to the next policy.
|
||||
:param state_updates: Ordered list of :class:`StateUpdate`
|
||||
operations to apply to the engine's ``session_state``.
|
||||
Each entry specifies a key, an action (``SET``,
|
||||
@@ -240,10 +245,19 @@ class PolicyResult:
|
||||
action: PolicyAction
|
||||
reason: str | None = None
|
||||
set_labels: dict[str, str] | None = None
|
||||
deciding_policy: str | None = None
|
||||
deciding_policies: list[str] | None = None
|
||||
data: Any = None
|
||||
state_updates: list[StateUpdate] | None = None
|
||||
|
||||
@property
|
||||
def deciding_policy(self) -> str | None:
|
||||
"""First deciding policy name, or ``None``.
|
||||
|
||||
Derived from ``deciding_policies[0]`` so callers that
|
||||
read ``.deciding_policy`` work without change.
|
||||
"""
|
||||
return self.deciding_policies[0] if self.deciding_policies else None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ElicitationRequest:
|
||||
@@ -272,10 +286,12 @@ class ElicitationRequest:
|
||||
e.g. ``"request"`` or ``"tool_call"``. Surfaces in the
|
||||
elicitation event's extras so the renderer can label the
|
||||
prompt.
|
||||
:param policy_name: Name of the deciding (first-in-YAML-order)
|
||||
ASKing policy. Drives per-policy ``ask_timeout`` lookup,
|
||||
observability, and the renderer's "policy X says..." label.
|
||||
e.g. ``"pii_redact"``.
|
||||
:param policy_names: Names of all ASKing policies that contributed
|
||||
to this elicitation, in YAML order. Always a non-empty list.
|
||||
``policy_name`` is a computed property returning
|
||||
``policy_names[0]`` — existing callers that read
|
||||
``.policy_name`` work without change.
|
||||
e.g. ``["pii_redact"]`` or ``["pii_redact", "cost_gate"]``.
|
||||
:param content_preview: Truncated snapshot of the content being
|
||||
gated. Lets a human reviewer see what they're approving
|
||||
without overwhelming the UI on a 50 KB payload. Surfaces in
|
||||
@@ -284,10 +300,19 @@ class ElicitationRequest:
|
||||
|
||||
message: str
|
||||
phase: str
|
||||
policy_name: str
|
||||
policy_names: list[str]
|
||||
content_preview: str
|
||||
requested_schema: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def policy_name(self) -> str:
|
||||
"""First ASKing policy name.
|
||||
|
||||
Derived from ``policy_names[0]`` so callers that read
|
||||
``.policy_name`` work without change.
|
||||
"""
|
||||
return self.policy_names[0] if self.policy_names else ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyLLMClient:
|
||||
|
||||
@@ -128,7 +128,7 @@ async def _await_elicitation(
|
||||
elicitation = ElicitationRequest(
|
||||
message=result.reason or "",
|
||||
phase=phase.value,
|
||||
policy_name=result.deciding_policy or "",
|
||||
policy_names=result.deciding_policies or [""],
|
||||
content_preview=_truncate(content_preview, limit=1024),
|
||||
)
|
||||
params_json = build_elicitation_params_json(elicitation)
|
||||
@@ -222,6 +222,8 @@ def build_elicitation_request_event(
|
||||
"policy_name": elicitation.policy_name,
|
||||
"content_preview": elicitation.content_preview,
|
||||
}
|
||||
if len(elicitation.policy_names) > 1:
|
||||
params["policy_names"] = elicitation.policy_names
|
||||
if url is not None:
|
||||
params["url"] = url
|
||||
|
||||
|
||||
@@ -238,12 +238,17 @@ class PolicyEngine:
|
||||
d. Action-list validation and the classifier-only
|
||||
carve-out for FunctionPolicy and PromptPolicy.
|
||||
e. Accumulate ``set_labels`` writes.
|
||||
f. If the policy returned ``data``, feed it back
|
||||
as ``ctx.content`` so the next policy transforms
|
||||
the already-transformed payload (sequential
|
||||
chaining across the pipeline).
|
||||
2. On DENY: short-circuit. Apply accumulated writes
|
||||
from any ALLOWing predecessors, then return the
|
||||
DENY result (with ``deciding_policy`` set).
|
||||
3. After the loop, if any policy ASKed: return an ASK
|
||||
result carrying accumulated (but unapplied)
|
||||
writes — the caller applies them only on approve
|
||||
writes and the full ``deciding_policies`` list —
|
||||
the caller applies them only on approve
|
||||
(POLICIES.md §7.2).
|
||||
4. Otherwise: apply writes, return ALLOW.
|
||||
|
||||
@@ -267,10 +272,11 @@ class PolicyEngine:
|
||||
accumulated: dict[str, str] = {}
|
||||
accumulated_state: list[StateUpdate] = []
|
||||
ask_reasons: list[str] = []
|
||||
deciding_ask_policy: str | None = None
|
||||
# Last non-None data from any ALLOW-or-ASK policy. If multiple
|
||||
# policies return data, the last one wins — callers that need
|
||||
# chained transforms should compose them in a single callable.
|
||||
deciding_ask_policies: list[str] = []
|
||||
# Sequentially accumulated data: each policy that returns data
|
||||
# has its output fed back into ctx.content so the next policy
|
||||
# in the chain transforms the already-transformed payload rather
|
||||
# than the original. The final value is the fully-composed result.
|
||||
composed_data: Any = None
|
||||
context = self._context()
|
||||
|
||||
@@ -308,12 +314,14 @@ class PolicyEngine:
|
||||
)
|
||||
if result.data is not None:
|
||||
composed_data = result.data
|
||||
# Feed the transformed payload forward so the next policy
|
||||
# in the chain sees this policy's output, not the original.
|
||||
ctx = replace(ctx, content=composed_data)
|
||||
if result.action == PolicyAction.ASK:
|
||||
ask_reasons.append(
|
||||
f"{policy.spec.name}: {result.reason or 'approval required'}",
|
||||
)
|
||||
if deciding_ask_policy is None:
|
||||
deciding_ask_policy = policy.spec.name
|
||||
deciding_ask_policies.append(policy.spec.name)
|
||||
|
||||
if ask_reasons:
|
||||
# DO NOT apply label writes or state updates here — the ASK
|
||||
@@ -326,7 +334,7 @@ class PolicyEngine:
|
||||
reason="; ".join(ask_reasons),
|
||||
set_labels=dict(accumulated) if accumulated else None,
|
||||
state_updates=list(accumulated_state) if accumulated_state else None,
|
||||
deciding_policy=deciding_ask_policy,
|
||||
deciding_policies=deciding_ask_policies,
|
||||
data=composed_data,
|
||||
)
|
||||
if not read_only:
|
||||
@@ -337,7 +345,6 @@ class PolicyEngine:
|
||||
reason=None,
|
||||
set_labels=dict(accumulated) if accumulated else None,
|
||||
state_updates=list(accumulated_state) if accumulated_state else None,
|
||||
deciding_policy=None,
|
||||
data=composed_data,
|
||||
)
|
||||
|
||||
@@ -381,7 +388,7 @@ class PolicyEngine:
|
||||
reason=reason,
|
||||
set_labels=dict(accumulated) if accumulated else None,
|
||||
state_updates=list(accumulated_state) if accumulated_state else None,
|
||||
deciding_policy=deciding_policy,
|
||||
deciding_policies=[deciding_policy],
|
||||
)
|
||||
|
||||
def _should_fire(
|
||||
|
||||
@@ -7563,19 +7563,30 @@ async def _forward_event_to_runner(
|
||||
_resolve_message_content,
|
||||
)
|
||||
|
||||
try:
|
||||
forwarded_data["content"] = _resolve_message_content(
|
||||
forwarded_data["content"],
|
||||
file_store,
|
||||
artifact_store,
|
||||
session_id=session_id,
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
_logger.warning(
|
||||
"File reference resolution failed for session=%s",
|
||||
session_id,
|
||||
exc_info=True,
|
||||
)
|
||||
_unresolved = [
|
||||
b for b in forwarded_data["content"] if isinstance(b, dict) and "file_id" in b
|
||||
]
|
||||
if _unresolved:
|
||||
try:
|
||||
forwarded_data["content"] = _resolve_message_content(
|
||||
forwarded_data["content"],
|
||||
file_store,
|
||||
artifact_store,
|
||||
session_id=session_id,
|
||||
)
|
||||
_logger.debug(
|
||||
"Resolved %d file_id block(s) for session=%s before forwarding",
|
||||
len(_unresolved),
|
||||
session_id,
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
_logger.warning(
|
||||
"File reference resolution failed for session=%s "
|
||||
"(unresolved file_id blocks will reach the runner unresolved — "
|
||||
"runner will attempt fallback resolution)",
|
||||
session_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Flatten SessionEventInput {type, data} into the runner's
|
||||
# discriminated-union shape {type, ...data_fields}. The runner's
|
||||
@@ -9012,7 +9023,7 @@ async def _register_policy_elicitation(
|
||||
message=result.reason or "Approval required",
|
||||
requested_schema={},
|
||||
phase=Phase.TOOL_CALL.value,
|
||||
policy_name=result.deciding_policy or "unknown",
|
||||
policy_names=result.deciding_policies or ["unknown"],
|
||||
content_preview=arguments_preview[:1024],
|
||||
)
|
||||
# Approval state lives on the runner (in-memory
|
||||
@@ -10601,6 +10612,8 @@ async def _create_session_from_existing_agent(
|
||||
user_id: str | None = None,
|
||||
permission_store: PermissionStore | None = None,
|
||||
liveness_lookup: Callable[[list[str]], dict[str, SessionLiveness]] | None = None,
|
||||
file_store: FileStore | None = None,
|
||||
artifact_store: ArtifactStore | None = None,
|
||||
) -> SessionResponse:
|
||||
"""
|
||||
Create a session bound to an already-registered agent.
|
||||
@@ -10626,6 +10639,10 @@ async def _create_session_from_existing_agent(
|
||||
``parent_session_id`` and session-scoped ``agent_id``.
|
||||
:param liveness_lookup: Optional session-scoped liveness lookup
|
||||
to populate ``SessionResponse.runner_online``.
|
||||
:param file_store: Optional file metadata store for resolving
|
||||
``file_id`` references in ``initial_items`` before forwarding
|
||||
to the runner.
|
||||
:param artifact_store: Optional binary content store for the same.
|
||||
:returns: The newly created session snapshot.
|
||||
:raises OmnigentError: 404 if no agent matches ``body.agent_id``;
|
||||
403/404 if ``parent_session_id`` or session-scoped ``agent_id``
|
||||
@@ -10898,6 +10915,8 @@ async def _create_session_from_existing_agent(
|
||||
conversation_store,
|
||||
runner_client,
|
||||
agent_name=agent.name,
|
||||
file_store=file_store,
|
||||
artifact_store=artifact_store,
|
||||
created_by=_attribution_user(user_id),
|
||||
)
|
||||
# Re-read rather than reusing the local ``conv``: the label-only branch
|
||||
@@ -12202,6 +12221,8 @@ def create_sessions_router(
|
||||
user_id=user_id,
|
||||
permission_store=permission_store,
|
||||
liveness_lookup=liveness_lookup,
|
||||
file_store=file_store,
|
||||
artifact_store=artifact_store,
|
||||
)
|
||||
# Notify the runner about the new session so it can resolve
|
||||
# the spec and cache sub_agent_name before the first turn.
|
||||
|
||||
@@ -113,7 +113,7 @@ def _composed_ask(
|
||||
action=PolicyAction.ASK,
|
||||
reason=reason,
|
||||
set_labels=set_labels,
|
||||
deciding_policy=deciding_policy,
|
||||
deciding_policies=[deciding_policy],
|
||||
)
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ def test_params_json_serializes_all_fields() -> None:
|
||||
req = ElicitationRequest(
|
||||
message="needs review",
|
||||
phase="tool_call",
|
||||
policy_name="confirm_shell",
|
||||
policy_names=["confirm_shell"],
|
||||
content_preview="ls -la",
|
||||
)
|
||||
data = json.loads(_params_json(req))
|
||||
@@ -274,7 +274,7 @@ def test_elicitation_request_event_shape() -> None:
|
||||
req = ElicitationRequest(
|
||||
message="needs review",
|
||||
phase="tool_call",
|
||||
policy_name="confirm_shell",
|
||||
policy_names=["confirm_shell"],
|
||||
content_preview="ls -la",
|
||||
)
|
||||
event = _elicitation_request_event("elicit_xyz", req)
|
||||
@@ -308,7 +308,7 @@ def test_elicitation_request_event_url_mode(monkeypatch: pytest.MonkeyPatch) ->
|
||||
req = ElicitationRequest(
|
||||
message="approve shell?",
|
||||
phase="tool_call",
|
||||
policy_name="shell_gate",
|
||||
policy_names=["shell_gate"],
|
||||
content_preview="rm -rf /",
|
||||
)
|
||||
event = _elicitation_request_event("elicit_abc", req, session_id="conv_123")
|
||||
@@ -328,7 +328,7 @@ def test_elicitation_request_event_form_mode_explicit(monkeypatch: pytest.Monkey
|
||||
req = ElicitationRequest(
|
||||
message="approve?",
|
||||
phase="tool_call",
|
||||
policy_name="gate",
|
||||
policy_names=["gate"],
|
||||
content_preview="ls",
|
||||
)
|
||||
event = _elicitation_request_event("elicit_abc", req, session_id="conv_123")
|
||||
@@ -348,7 +348,7 @@ def test_elicitation_request_event_no_session_id_stays_form(
|
||||
req = ElicitationRequest(
|
||||
message="approve?",
|
||||
phase="tool_call",
|
||||
policy_name="gate",
|
||||
policy_names=["gate"],
|
||||
content_preview="",
|
||||
)
|
||||
event = _elicitation_request_event("elicit_abc", req)
|
||||
|
||||
@@ -317,8 +317,10 @@ async def test_ask_cycle_multiple_askers_combined_approval(
|
||||
|
||||
result, approved = await _run_ask_cycle(engine, ctx, harness)
|
||||
assert approved is True
|
||||
# First-ASKer-in-YAML wins deciding_policy.
|
||||
# deciding_policy is derived from deciding_policies[0].
|
||||
assert result.deciding_policy == "first"
|
||||
# All three ASKing policies are captured in deciding_policies.
|
||||
assert result.deciding_policies == ["first", "second", "third"]
|
||||
# Combined reason mentions all three policies.
|
||||
assert "first:" in result.reason
|
||||
assert "second:" in result.reason
|
||||
|
||||
@@ -1092,28 +1092,36 @@ async def test_engine_propagates_data_to_composed_allow(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_engine_last_data_wins_across_multiple_policies(
|
||||
async def test_engine_data_chains_sequentially_across_policies(
|
||||
conversation_store: SqlAlchemyConversationStore,
|
||||
) -> None:
|
||||
"""When multiple policies return ``data``, the last one wins.
|
||||
"""Each policy that returns ``data`` receives the previous
|
||||
policy's output as ``event["data"]`` (i.e. ``ctx.content``),
|
||||
not the original content.
|
||||
|
||||
Rationale: each subsequent policy in the chain operates on
|
||||
the context, and the final transform is the one the enforcement
|
||||
site should apply. Callers that need ordered chaining must
|
||||
compose that in a single callable.
|
||||
The canonical use case: a two-stage redaction pipeline where
|
||||
the first policy scrubs PII and the second strips secrets —
|
||||
the second policy must see the already-PII-scrubbed payload,
|
||||
not the raw original.
|
||||
"""
|
||||
first_data = {"query": "first-transform"}
|
||||
last_data = {"query": "last-transform"}
|
||||
seen_by_second: list[dict] = []
|
||||
|
||||
def first(_event: dict) -> PolicyResult:
|
||||
return PolicyResult(
|
||||
action=PolicyAction.ALLOW,
|
||||
data={"query": "after-first"},
|
||||
)
|
||||
|
||||
def second(event: dict) -> PolicyResult:
|
||||
seen_by_second.append(event["data"])
|
||||
return PolicyResult(
|
||||
action=PolicyAction.ALLOW,
|
||||
data={"query": "after-second"},
|
||||
)
|
||||
|
||||
policies = [
|
||||
FunctionPolicy(
|
||||
_spec(name="first", phase=Phase.TOOL_CALL),
|
||||
lambda event: PolicyResult(action=PolicyAction.ALLOW, data=first_data),
|
||||
),
|
||||
FunctionPolicy(
|
||||
_spec(name="last", phase=Phase.TOOL_CALL),
|
||||
lambda event: PolicyResult(action=PolicyAction.ALLOW, data=last_data),
|
||||
),
|
||||
FunctionPolicy(_spec(name="first", phase=Phase.TOOL_CALL), first),
|
||||
FunctionPolicy(_spec(name="second", phase=Phase.TOOL_CALL), second),
|
||||
]
|
||||
engine = _build_engine(conversation_store, policies)
|
||||
result = await engine.evaluate(
|
||||
@@ -1124,10 +1132,13 @@ async def test_engine_last_data_wins_across_multiple_policies(
|
||||
)
|
||||
)
|
||||
assert result.action == PolicyAction.ALLOW
|
||||
assert result.data == last_data, (
|
||||
f"Last policy's data must win; got {result.data!r}. "
|
||||
f"If 'first-transform', the engine kept the first data instead of the last."
|
||||
# The second policy must have seen the first policy's output.
|
||||
assert seen_by_second == [{"query": "after-first"}], (
|
||||
f"Second policy must receive first policy's data as content; "
|
||||
f"got {seen_by_second!r}. If 'original', the engine didn't chain."
|
||||
)
|
||||
# The composed result carries the last transform in the chain.
|
||||
assert result.data == {"query": "after-second"}
|
||||
|
||||
|
||||
# ── Gap 7: legacy (content, phase) callable shim ─────────────────────────────
|
||||
|
||||
@@ -42,6 +42,7 @@ def test_policy_result_defaults() -> None:
|
||||
assert r.reason is None
|
||||
assert r.set_labels is None
|
||||
assert r.deciding_policy is None
|
||||
assert r.deciding_policies is None
|
||||
|
||||
|
||||
def test_policy_result_full_construction() -> None:
|
||||
@@ -50,13 +51,13 @@ def test_policy_result_full_construction() -> None:
|
||||
action=PolicyAction.DENY,
|
||||
reason="blocked",
|
||||
set_labels={"a": "1"},
|
||||
deciding_policy="p",
|
||||
deciding_policies=["p"],
|
||||
)
|
||||
r2 = PolicyResult(
|
||||
action=PolicyAction.DENY,
|
||||
reason="blocked",
|
||||
set_labels={"a": "1"},
|
||||
deciding_policy="p",
|
||||
deciding_policies=["p"],
|
||||
)
|
||||
# Equality is structural — two results with the same
|
||||
# fields compare equal.
|
||||
@@ -71,12 +72,14 @@ def test_policy_result_inequality_on_action() -> None:
|
||||
|
||||
|
||||
def test_policy_result_inequality_on_deciding_policy() -> None:
|
||||
"""deciding_policy participates in equality — used by
|
||||
observability to distinguish identical-reason results
|
||||
from different sources."""
|
||||
a = PolicyResult(action=PolicyAction.DENY, deciding_policy="a")
|
||||
b = PolicyResult(action=PolicyAction.DENY, deciding_policy="b")
|
||||
"""deciding_policy (derived from deciding_policies[0]) participates
|
||||
in equality via deciding_policies — used by observability to
|
||||
distinguish identical-reason results from different sources."""
|
||||
a = PolicyResult(action=PolicyAction.DENY, deciding_policies=["a"])
|
||||
b = PolicyResult(action=PolicyAction.DENY, deciding_policies=["b"])
|
||||
assert a != b
|
||||
assert a.deciding_policy == "a"
|
||||
assert b.deciding_policy == "b"
|
||||
|
||||
|
||||
# ── Evaluation context shape ──────────────────────────
|
||||
|
||||
@@ -254,7 +254,7 @@ async def test_forged_retry_with_ask_policy_rejects_unknown_elicitation(
|
||||
result=PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="approval required",
|
||||
deciding_policy="test-gate",
|
||||
deciding_policies=["test-gate"],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -420,7 +420,7 @@ async def test_legitimate_retry_with_pending_entry_proceeds(
|
||||
result=PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="approval required",
|
||||
deciding_policy="test-gate",
|
||||
deciding_policies=["test-gate"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ async def test_pending_verdict_registers_elicitation():
|
||||
ask_result = PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="Requires user approval",
|
||||
deciding_policy="approve_shell",
|
||||
deciding_policies=["approve_shell"],
|
||||
)
|
||||
|
||||
async def _eval(_ctx: Any) -> PolicyResult:
|
||||
@@ -362,7 +362,7 @@ async def test_pending_verdict_carries_per_policy_ask_timeout():
|
||||
ask_result = PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="Requires user approval",
|
||||
deciding_policy="approve_shell",
|
||||
deciding_policies=["approve_shell"],
|
||||
)
|
||||
|
||||
async def _eval(_ctx: Any) -> PolicyResult:
|
||||
@@ -685,7 +685,7 @@ async def test_input_ask_approved_falls_through_to_allow():
|
||||
ask_result = PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="Deleting files requires approval",
|
||||
deciding_policy="llm_prompt_classifier_policy",
|
||||
deciding_policies=["llm_prompt_classifier_policy"],
|
||||
)
|
||||
|
||||
async def _eval(_ctx: Any) -> PolicyResult:
|
||||
@@ -763,7 +763,7 @@ async def test_input_ask_declined_denies():
|
||||
ask_result = PolicyResult(
|
||||
action=PolicyAction.ASK,
|
||||
reason="Deleting files requires approval",
|
||||
deciding_policy="llm_prompt_classifier_policy",
|
||||
deciding_policies=["llm_prompt_classifier_policy"],
|
||||
)
|
||||
|
||||
async def _eval(_ctx: Any) -> PolicyResult:
|
||||
|
||||
Reference in New Issue
Block a user