ory-agent-framework 0.13.9__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.
- ory_agent_framework-0.13.9/.gitignore +36 -0
- ory_agent_framework-0.13.9/PKG-INFO +43 -0
- ory_agent_framework-0.13.9/README.md +28 -0
- ory_agent_framework-0.13.9/pyproject.toml +25 -0
- ory_agent_framework-0.13.9/src/ory_agent_framework/__init__.py +18 -0
- ory_agent_framework-0.13.9/src/ory_agent_framework/middleware.py +85 -0
- ory_agent_framework-0.13.9/tests/test_middleware.py +95 -0
- ory_agent_framework-0.13.9/tests/test_realsdk.py +48 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
*.tsbuildinfo
|
|
4
|
+
.env
|
|
5
|
+
.env.local
|
|
6
|
+
|
|
7
|
+
# Python (uv workspace under python/)
|
|
8
|
+
.venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
build/
|
|
14
|
+
*.pyc
|
|
15
|
+
|
|
16
|
+
# Debug logs
|
|
17
|
+
*.log
|
|
18
|
+
|
|
19
|
+
# Harness sandbox dirs
|
|
20
|
+
.sandbox/
|
|
21
|
+
|
|
22
|
+
# Staged install-surface repo contents (see scripts/sync-install-surfaces.mjs)
|
|
23
|
+
.install-surfaces/
|
|
24
|
+
|
|
25
|
+
# Local dev environment (local Ory stack + Verdaccio registry)
|
|
26
|
+
.ory-dev/
|
|
27
|
+
|
|
28
|
+
# Worktrees for changes
|
|
29
|
+
.worktrees/
|
|
30
|
+
.claude/worktrees/
|
|
31
|
+
|
|
32
|
+
# Gemini CLI extension assets — materialized at install time from
|
|
33
|
+
# @ory/argus templates. The repo source is the canonical templates;
|
|
34
|
+
# these subdirs are generated wherever the install runs.
|
|
35
|
+
packages/gemini-cli/gemini-extension/skills/
|
|
36
|
+
packages/gemini-cli/gemini-extension/commands/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ory-agent-framework
|
|
3
|
+
Version: 0.13.9
|
|
4
|
+
Summary: Ory Agent Security for the Microsoft Agent Framework — per-tool authorization, tracing, and identity propagation via function middleware. Built on ory-argus.
|
|
5
|
+
Author: Ory
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: agent,agent-framework,ai,authorization,autogen,microsoft,ory
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: agent-framework>=0.0.0a1
|
|
10
|
+
Requires-Dist: ory-argus<1,>=0.8
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ory-agent-framework
|
|
17
|
+
|
|
18
|
+
Ory Agent Security for the [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/)
|
|
19
|
+
(the successor to AutoGen + Semantic Kernel).
|
|
20
|
+
|
|
21
|
+
Gates every function/tool call through Ory Permissions via **function middleware**, traces
|
|
22
|
+
invocations, and propagates the user → agent identity — built on
|
|
23
|
+
[`ory-argus`](https://pypi.org/project/ory-argus/).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install ory-agent-framework
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from ory_agent_framework import ory_function_middleware
|
|
31
|
+
|
|
32
|
+
agent = chat_client.create_agent(
|
|
33
|
+
instructions="…",
|
|
34
|
+
tools=[search, send_email],
|
|
35
|
+
middleware=[ory_function_middleware()],
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a denied call sets the result to the
|
|
40
|
+
denial and terminates without invoking the function. In **observe** mode (default) the
|
|
41
|
+
function runs and a `permission.observe_deny` span is recorded.
|
|
42
|
+
|
|
43
|
+
Credentials come from the shared `~/.config/ory-agent-plugins/config.json`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ory-agent-framework
|
|
2
|
+
|
|
3
|
+
Ory Agent Security for the [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/)
|
|
4
|
+
(the successor to AutoGen + Semantic Kernel).
|
|
5
|
+
|
|
6
|
+
Gates every function/tool call through Ory Permissions via **function middleware**, traces
|
|
7
|
+
invocations, and propagates the user → agent identity — built on
|
|
8
|
+
[`ory-argus`](https://pypi.org/project/ory-argus/).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install ory-agent-framework
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from ory_agent_framework import ory_function_middleware
|
|
16
|
+
|
|
17
|
+
agent = chat_client.create_agent(
|
|
18
|
+
instructions="…",
|
|
19
|
+
tools=[search, send_email],
|
|
20
|
+
middleware=[ory_function_middleware()],
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a denied call sets the result to the
|
|
25
|
+
denial and terminates without invoking the function. In **observe** mode (default) the
|
|
26
|
+
function runs and a `permission.observe_deny` span is recorded.
|
|
27
|
+
|
|
28
|
+
Credentials come from the shared `~/.config/ory-agent-plugins/config.json`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ory-agent-framework"
|
|
7
|
+
version = "0.13.9"
|
|
8
|
+
description = "Ory Agent Security for the Microsoft Agent Framework — per-tool authorization, tracing, and identity propagation via function middleware. Built on ory-argus."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Ory" }]
|
|
13
|
+
keywords = ["ory", "microsoft", "agent-framework", "autogen", "authorization", "agent", "ai"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"ory-argus>=0.8,<1",
|
|
16
|
+
# The Microsoft Agent Framework currently ships pre-release builds on PyPI; the
|
|
17
|
+
# pre-release marker lets `pip install ory-agent-framework` resolve them.
|
|
18
|
+
"agent-framework>=0.0.0a1",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/ory_agent_framework"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Ory Agent Security for the Microsoft Agent Framework.
|
|
2
|
+
|
|
3
|
+
from ory_agent_framework import ory_function_middleware
|
|
4
|
+
|
|
5
|
+
agent = chat_client.create_agent(
|
|
6
|
+
instructions="…",
|
|
7
|
+
tools=[search, send_email],
|
|
8
|
+
middleware=[ory_function_middleware()],
|
|
9
|
+
)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .middleware import guarded_process, ory_function_middleware
|
|
15
|
+
|
|
16
|
+
__version__ = "0.13.9"
|
|
17
|
+
|
|
18
|
+
__all__ = ["ory_function_middleware", "guarded_process", "__version__"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Ory Agent Security for the Microsoft Agent Framework.
|
|
2
|
+
|
|
3
|
+
The Agent Framework's per-tool interceptor is **function middleware** — ``process(context,
|
|
4
|
+
next)``. ``ory_function_middleware()`` authorizes each function/tool call against Ory
|
|
5
|
+
Permissions, traces it, and (on first call) runs the Ory session gates:
|
|
6
|
+
|
|
7
|
+
- allow / observe / fail-open / interactive → call ``next`` (the function runs) and record
|
|
8
|
+
``tool.complete``.
|
|
9
|
+
- deny (enforce) → set ``context.result`` to the denial and ``context.terminate = True``
|
|
10
|
+
*without* calling ``next``, so the function never runs.
|
|
11
|
+
|
|
12
|
+
Gate logic lives in ``ory_argus``. ``agent_framework`` is imported lazily, so importing this
|
|
13
|
+
module never requires it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from ory_argus import GateResult, OryAgentClient, complete, gate, session_start
|
|
21
|
+
|
|
22
|
+
HARNESS = "agent-framework"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract(context: Any) -> tuple[str, Any]:
|
|
26
|
+
fn = getattr(context, "function", None)
|
|
27
|
+
name = (
|
|
28
|
+
getattr(fn, "name", None)
|
|
29
|
+
or getattr(context, "function_name", None)
|
|
30
|
+
or getattr(context, "name", None)
|
|
31
|
+
or "unknown"
|
|
32
|
+
)
|
|
33
|
+
args = getattr(context, "arguments", None) or getattr(context, "args", None)
|
|
34
|
+
return name, args
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def guarded_process(
|
|
38
|
+
client: OryAgentClient,
|
|
39
|
+
context: Any,
|
|
40
|
+
call_next: Any,
|
|
41
|
+
*,
|
|
42
|
+
can_block: bool = True,
|
|
43
|
+
project_url: str | None = None,
|
|
44
|
+
_session_state: dict | None = None,
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""SDK-free guarded middleware body — testable with a fake context + next."""
|
|
47
|
+
state = _session_state if _session_state is not None else {}
|
|
48
|
+
if not state.get("started"):
|
|
49
|
+
state["started"] = True
|
|
50
|
+
try:
|
|
51
|
+
session_start(client, harness=HARNESS, project_url=project_url)
|
|
52
|
+
except Exception as err: # noqa: BLE001
|
|
53
|
+
client.logger.warn("session_start.failed", {"message": str(err)})
|
|
54
|
+
|
|
55
|
+
name, args = _extract(context)
|
|
56
|
+
result: GateResult = gate(client, harness=HARNESS, tool_name=name, tool_args=args, can_block=can_block)
|
|
57
|
+
if result.blocked:
|
|
58
|
+
# Short-circuit: set the result and terminate without calling next.
|
|
59
|
+
context.result = result.denial_message or "Ory: permission denied"
|
|
60
|
+
try:
|
|
61
|
+
context.terminate = True
|
|
62
|
+
except Exception: # noqa: BLE001 — some contexts may not expose terminate
|
|
63
|
+
pass
|
|
64
|
+
return None
|
|
65
|
+
out = await call_next(context)
|
|
66
|
+
complete(client, tool_name=name)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ory_function_middleware(
|
|
71
|
+
*, client: OryAgentClient | None = None, can_block: bool = True, project_url: str | None = None
|
|
72
|
+
):
|
|
73
|
+
"""Return a function-middleware callable ``async (context, next)`` gated through Ory."""
|
|
74
|
+
c = client or OryAgentClient.from_env(HARNESS)
|
|
75
|
+
state: dict = {}
|
|
76
|
+
|
|
77
|
+
async def _middleware(context: Any, call_next: Any) -> Any:
|
|
78
|
+
return await guarded_process(
|
|
79
|
+
c, context, call_next, can_block=can_block, project_url=project_url, _session_state=state
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return _middleware
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["HARNESS", "guarded_process", "ory_function_middleware"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Hermetic tests for the Microsoft Agent Framework Ory function middleware (SDK-free).
|
|
2
|
+
|
|
3
|
+
Covers the native veto shape (``context.terminate`` + ``context.result`` without calling
|
|
4
|
+
``next``) and request-shape extraction (``function.name`` and the ``function_name``
|
|
5
|
+
fallback). The full permission decision matrix is core-owned
|
|
6
|
+
(``ory-argus/tests/test_adapters.py``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from ory_agent_framework import guarded_process # type: ignore
|
|
17
|
+
from ory_argus.client import PrincipalIdentity
|
|
18
|
+
from ory_argus.testing import (
|
|
19
|
+
create_mock_client,
|
|
20
|
+
get_trace_spans,
|
|
21
|
+
stub_permission_allowed,
|
|
22
|
+
stub_permission_denied,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture(autouse=True)
|
|
27
|
+
def _isolate(monkeypatch, tmp_path):
|
|
28
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
29
|
+
monkeypatch.delenv("ORY_PERMISSION_MODE", raising=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def client_with_user():
|
|
33
|
+
c = create_mock_client(harness="agent-framework")
|
|
34
|
+
c.set_user_principal(PrincipalIdentity(subject="user:alice"))
|
|
35
|
+
return c
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_context(name="search", args=None):
|
|
39
|
+
return SimpleNamespace(function=SimpleNamespace(name=name), arguments=args or {}, result=None, terminate=False)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run(coro):
|
|
43
|
+
return asyncio.run(coro)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_allow_calls_next_and_completes():
|
|
47
|
+
c = client_with_user()
|
|
48
|
+
stub_permission_allowed(c)
|
|
49
|
+
ctx = make_context("search")
|
|
50
|
+
ran = {}
|
|
51
|
+
|
|
52
|
+
async def call_next(context):
|
|
53
|
+
ran["yes"] = True
|
|
54
|
+
return "ok"
|
|
55
|
+
|
|
56
|
+
out = run(guarded_process(c, ctx, call_next))
|
|
57
|
+
assert out == "ok"
|
|
58
|
+
assert ran.get("yes") is True
|
|
59
|
+
invoke = get_trace_spans(c, "tool.invoke")[0]
|
|
60
|
+
assert invoke.attributes["allowed"] is True
|
|
61
|
+
# Tool name pulled from the context's ``function.name``.
|
|
62
|
+
assert invoke.attributes["toolName"] == "search"
|
|
63
|
+
assert get_trace_spans(c, "tool.complete")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_enforce_terminates_without_next(monkeypatch):
|
|
67
|
+
monkeypatch.setenv("ORY_PERMISSION_MODE", "enforce")
|
|
68
|
+
c = client_with_user()
|
|
69
|
+
stub_permission_denied(c)
|
|
70
|
+
ctx = make_context("rm")
|
|
71
|
+
ran = {}
|
|
72
|
+
|
|
73
|
+
async def call_next(context):
|
|
74
|
+
ran["yes"] = True
|
|
75
|
+
return "should-not-run"
|
|
76
|
+
|
|
77
|
+
out = run(guarded_process(c, ctx, call_next))
|
|
78
|
+
# Native veto shape: next() skipped, terminate flag set, denial in context.result.
|
|
79
|
+
assert out is None
|
|
80
|
+
assert "yes" not in ran # next never called
|
|
81
|
+
assert ctx.terminate is True
|
|
82
|
+
assert "permission denied" in ctx.result
|
|
83
|
+
assert get_trace_spans(c, "tool.block")[0].attributes["blocked"] is True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_extract_handles_function_name_field():
|
|
87
|
+
c = client_with_user()
|
|
88
|
+
stub_permission_allowed(c)
|
|
89
|
+
ctx = SimpleNamespace(function_name="edit", args={"a": 1}, result=None, terminate=False)
|
|
90
|
+
|
|
91
|
+
async def call_next(context):
|
|
92
|
+
return "ok"
|
|
93
|
+
|
|
94
|
+
run(guarded_process(c, ctx, call_next))
|
|
95
|
+
assert get_trace_spans(c, "tool.invoke")[0].attributes["toolName"] == "edit"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Real-SDK test: the Microsoft Agent Framework middleware builds + vetoes.
|
|
2
|
+
|
|
3
|
+
Runs only when ``agent_framework`` is importable; the default workspace venv skips it. The
|
|
4
|
+
framework's veto is plain attribute setting (no SDK type), so the real-SDK value here is that
|
|
5
|
+
the module + middleware import and construct against the installed SDK. The permission
|
|
6
|
+
decision is stubbed (no backend).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
pytest.importorskip("agent_framework")
|
|
17
|
+
|
|
18
|
+
from ory_agent_framework import guarded_process, ory_function_middleware # noqa: E402
|
|
19
|
+
from ory_agent_framework import middleware as m # noqa: E402
|
|
20
|
+
from ory_argus.adapters import GateResult # noqa: E402
|
|
21
|
+
from ory_argus.permissions import PermissionDecision # noqa: E402
|
|
22
|
+
from ory_argus.testing import create_mock_client # noqa: E402
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_middleware_builds_against_real_sdk():
|
|
26
|
+
assert callable(ory_function_middleware(client=create_mock_client(harness="agent-framework")))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_deny_terminates_without_next(monkeypatch):
|
|
30
|
+
monkeypatch.setattr(m, "session_start", lambda *a, **k: None)
|
|
31
|
+
monkeypatch.setattr(
|
|
32
|
+
m, "gate",
|
|
33
|
+
lambda client, **kw: GateResult(
|
|
34
|
+
proceed=False, blocked=True, decision=PermissionDecision(kind="deny"),
|
|
35
|
+
subject="User:t", namespace="AgentTools", denial_message="Ory: permission denied",
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
ctx = SimpleNamespace(function=SimpleNamespace(name="MyRealTool"), arguments={}, result=None, terminate=False)
|
|
39
|
+
ran = {}
|
|
40
|
+
|
|
41
|
+
async def call_next(_ctx):
|
|
42
|
+
ran["yes"] = True
|
|
43
|
+
return "should-not-run"
|
|
44
|
+
|
|
45
|
+
asyncio.run(guarded_process(create_mock_client(harness="agent-framework"), ctx, call_next))
|
|
46
|
+
assert "yes" not in ran
|
|
47
|
+
assert ctx.terminate is True
|
|
48
|
+
assert "permission denied" in str(ctx.result)
|