ory-strands 0.12.1__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,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-strands
3
+ Version: 0.12.1
4
+ Summary: Ory Agent Security for AWS Strands Agents — per-tool authorization, tracing, and identity propagation via tool-invocation hooks. Built on ory-argus.
5
+ Author: Ory
6
+ License-Expression: Apache-2.0
7
+ Keywords: agent,ai,authorization,aws,bedrock,ory,permissions,strands
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: ory-argus<1,>=0.8
10
+ Requires-Dist: strands-agents>=1.0
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-strands
17
+
18
+ Ory Agent Security for [AWS Strands Agents](https://strandsagents.com/).
19
+
20
+ Authorizes every tool call against Ory Permissions via Strands' tool-invocation hooks,
21
+ traces invocations, and propagates user → agent identity — built on
22
+ [`ory-argus`](https://pypi.org/project/ory-argus/).
23
+
24
+ ```bash
25
+ pip install ory-strands
26
+ ```
27
+
28
+ ```python
29
+ from strands import Agent
30
+ from ory_strands import OryHooks
31
+
32
+ agent = Agent(model=..., tools=[search, send_email], hooks=[OryHooks()])
33
+ ```
34
+
35
+ `OryHooks` subscribes to `BeforeToolCallEvent` (authorize) and
36
+ `AfterToolCallEvent` (trace). In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a
37
+ denied tool is **cancelled** — Strands returns the denial as an error tool result and the
38
+ tool never runs. In **observe** mode (default) the tool runs and a `permission.observe_deny`
39
+ span is recorded.
40
+
41
+ Credentials come from the shared `~/.config/ory-agent-plugins/config.json`, so a login from
42
+ any Ory harness or `ory-argus login` is reused automatically. See
43
+ [docs/sdk-integrations.md](https://github.com/ory-corp/ory-agent-plugins/blob/main/docs/sdk-integrations.md).
@@ -0,0 +1,28 @@
1
+ # ory-strands
2
+
3
+ Ory Agent Security for [AWS Strands Agents](https://strandsagents.com/).
4
+
5
+ Authorizes every tool call against Ory Permissions via Strands' tool-invocation hooks,
6
+ traces invocations, and propagates user → agent identity — built on
7
+ [`ory-argus`](https://pypi.org/project/ory-argus/).
8
+
9
+ ```bash
10
+ pip install ory-strands
11
+ ```
12
+
13
+ ```python
14
+ from strands import Agent
15
+ from ory_strands import OryHooks
16
+
17
+ agent = Agent(model=..., tools=[search, send_email], hooks=[OryHooks()])
18
+ ```
19
+
20
+ `OryHooks` subscribes to `BeforeToolCallEvent` (authorize) and
21
+ `AfterToolCallEvent` (trace). In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a
22
+ denied tool is **cancelled** — Strands returns the denial as an error tool result and the
23
+ tool never runs. In **observe** mode (default) the tool runs and a `permission.observe_deny`
24
+ span is recorded.
25
+
26
+ Credentials come from the shared `~/.config/ory-agent-plugins/config.json`, so a login from
27
+ any Ory harness or `ory-argus login` is reused automatically. See
28
+ [docs/sdk-integrations.md](https://github.com/ory-corp/ory-agent-plugins/blob/main/docs/sdk-integrations.md).
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ory-strands"
7
+ version = "0.12.1"
8
+ description = "Ory Agent Security for AWS Strands Agents — per-tool authorization, tracing, and identity propagation via tool-invocation hooks. 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", "strands", "aws", "bedrock", "authorization", "agent", "ai", "permissions"]
14
+ dependencies = [
15
+ "ory-argus>=0.8,<1",
16
+ "strands-agents>=1.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=8", "ruff>=0.6"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/ory_strands"]
@@ -0,0 +1,15 @@
1
+ """Ory Agent Security for AWS Strands Agents.
2
+
3
+ from strands import Agent
4
+ from ory_strands import OryHooks
5
+
6
+ agent = Agent(model=..., tools=[...], hooks=[OryHooks()])
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .hooks import OryHooks, apply_after, apply_before
12
+
13
+ __version__ = "0.12.1"
14
+
15
+ __all__ = ["OryHooks", "apply_before", "apply_after", "__version__"]
@@ -0,0 +1,115 @@
1
+ """Ory Agent Security for AWS Strands Agents.
2
+
3
+ Strands exposes a first-class per-tool gate through its hook system:
4
+ ``BeforeToolCallEvent`` fires before a tool runs (and a hook can **cancel** it by setting
5
+ ``event.cancel_tool``), and ``AfterToolCallEvent`` fires after. ``OryHooks`` is a Strands
6
+ ``HookProvider`` that authorizes each call against Ory Permissions:
7
+
8
+ - allow / observe / fail-open / interactive → do nothing (the tool runs).
9
+ - deny (enforce) → set ``event.cancel_tool = "Ory: permission denied …"`` so Strands skips
10
+ the tool and returns the denial as an error tool result.
11
+
12
+ Register it on the agent:
13
+
14
+ from strands import Agent
15
+ from ory_strands import OryHooks
16
+
17
+ agent = Agent(model=..., tools=[...], hooks=[OryHooks()])
18
+
19
+ All gate logic lives in ``ory_argus``; the hook callbacks are duck-typed (read
20
+ ``event.tool_use`` and set ``event.cancel_tool``), so this module imports without
21
+ ``strands-agents`` present — only registering the hooks touches the SDK.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ from ory_argus import OryAgentClient, complete, gate, session_start
29
+
30
+ HARNESS = "strands"
31
+
32
+
33
+ def _tool_name(event: Any) -> str:
34
+ tu = getattr(event, "tool_use", None) or {}
35
+ try:
36
+ return tu.get("name") or "unknown"
37
+ except AttributeError:
38
+ return getattr(tu, "name", None) or "unknown"
39
+
40
+
41
+ def _tool_input(event: Any) -> Any:
42
+ tu = getattr(event, "tool_use", None) or {}
43
+ try:
44
+ return tu.get("input")
45
+ except AttributeError:
46
+ return getattr(tu, "input", None)
47
+
48
+
49
+ def apply_before(
50
+ client: OryAgentClient,
51
+ event: Any,
52
+ *,
53
+ can_block: bool = True,
54
+ project_url: str | None = None,
55
+ _session_state: dict | None = None,
56
+ ) -> None:
57
+ """Gate a ``BeforeToolCallEvent``. On deny, sets ``event.cancel_tool``. SDK-free."""
58
+ state = _session_state if _session_state is not None else {}
59
+ if not state.get("started"):
60
+ state["started"] = True
61
+ try:
62
+ session_start(client, harness=HARNESS, project_url=project_url)
63
+ except Exception as err: # noqa: BLE001 — session gate must never break the agent
64
+ client.logger.warn("session_start.failed", {"message": str(err)})
65
+
66
+ result = gate(
67
+ client, harness=HARNESS, tool_name=_tool_name(event), tool_args=_tool_input(event), can_block=can_block
68
+ )
69
+ if result.blocked:
70
+ event.cancel_tool = result.denial_message or "Ory: permission denied"
71
+
72
+
73
+ def apply_after(client: OryAgentClient, event: Any) -> None:
74
+ """Record a ``tool.complete`` span for an ``AfterToolCallEvent``. SDK-free."""
75
+ complete(client, tool_name=_tool_name(event), output=getattr(event, "result", None))
76
+
77
+
78
+ class OryHooks:
79
+ """Strands ``HookProvider`` that gates every tool call through Ory.
80
+
81
+ Args:
82
+ client: An ``OryAgentClient``. Defaults to ``OryAgentClient.from_env("strands")``.
83
+ can_block: Whether a denied tool is hard-cancelled. Default ``True``.
84
+ project_url: Override the Ory project URL for the session gates.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ client: OryAgentClient | None = None,
91
+ can_block: bool = True,
92
+ project_url: str | None = None,
93
+ ) -> None:
94
+ self._client = client or OryAgentClient.from_env(HARNESS)
95
+ self._can_block = can_block
96
+ self._project_url = project_url
97
+ self._state: dict = {}
98
+
99
+ def register_hooks(self, registry: Any, **kwargs: Any) -> None:
100
+ from strands.hooks.events import AfterToolCallEvent, BeforeToolCallEvent
101
+
102
+ registry.add_callback(BeforeToolCallEvent, self._before)
103
+ registry.add_callback(AfterToolCallEvent, self._after)
104
+
105
+ def _before(self, event: Any) -> None:
106
+ apply_before(
107
+ self._client, event, can_block=self._can_block,
108
+ project_url=self._project_url, _session_state=self._state,
109
+ )
110
+
111
+ def _after(self, event: Any) -> None:
112
+ apply_after(self._client, event)
113
+
114
+
115
+ __all__ = ["HARNESS", "OryHooks", "apply_before", "apply_after"]
@@ -0,0 +1,70 @@
1
+ """Hermetic tests for the Strands tool-invocation hooks (mocked Ory, fake events).
2
+
3
+ Covers the native veto shape (mutating ``event.cancel_tool`` to the denial string) and
4
+ request-shape extraction (``tool_use["name"]`` on both the before and after events). The
5
+ full permission decision matrix is core-owned (``ory-argus/tests/test_adapters.py``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from types import SimpleNamespace
11
+
12
+ import pytest
13
+
14
+ from ory_argus.client import PrincipalIdentity
15
+ from ory_argus.testing import (
16
+ create_mock_client,
17
+ get_trace_spans,
18
+ stub_permission_allowed,
19
+ stub_permission_denied,
20
+ )
21
+ from ory_strands import apply_after, apply_before
22
+ from ory_strands import hooks as hooks_mod
23
+
24
+
25
+ @pytest.fixture(autouse=True)
26
+ def _isolate(monkeypatch, tmp_path):
27
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
28
+ monkeypatch.delenv("ORY_PERMISSION_MODE", raising=False)
29
+ monkeypatch.setattr(hooks_mod, "session_start", lambda *a, **k: None)
30
+
31
+
32
+ def client_with_user():
33
+ c = create_mock_client(harness="strands")
34
+ c.set_user_principal(PrincipalIdentity(subject="user:alice"))
35
+ return c
36
+
37
+
38
+ def _before_event(name="search"):
39
+ return SimpleNamespace(tool_use={"name": name, "input": {"q": 1}}, cancel_tool=None)
40
+
41
+
42
+ def test_allow_does_not_cancel():
43
+ c = client_with_user()
44
+ stub_permission_allowed(c)
45
+ ev = _before_event("search")
46
+ apply_before(c, ev)
47
+ assert ev.cancel_tool is None
48
+ invoke = get_trace_spans(c, "tool.invoke")[0]
49
+ assert invoke.attributes["allowed"] is True
50
+ # Tool name pulled from the event's ``tool_use["name"]``.
51
+ assert invoke.attributes["toolName"] == "search"
52
+
53
+
54
+ def test_enforce_cancels_tool(monkeypatch):
55
+ monkeypatch.setenv("ORY_PERMISSION_MODE", "enforce")
56
+ c = client_with_user()
57
+ stub_permission_denied(c)
58
+ ev = _before_event("rm")
59
+ apply_before(c, ev)
60
+ # Native veto shape: cancel_tool mutated to the denial string, which Strands uses
61
+ # to cancel the invocation before the tool runs.
62
+ assert isinstance(ev.cancel_tool, str) and "permission denied" in ev.cancel_tool.lower()
63
+ assert get_trace_spans(c, "tool.block")[0].attributes["blocked"] is True
64
+
65
+
66
+ def test_after_records_complete():
67
+ c = client_with_user()
68
+ ev = SimpleNamespace(tool_use={"name": "search"}, result="ok")
69
+ apply_after(c, ev)
70
+ assert get_trace_spans(c, "tool.complete")[0].attributes["toolName"] == "search"
@@ -0,0 +1,35 @@
1
+ """Real-SDK test: OryHooks registers against the real Strands hook events.
2
+
3
+ Runs only when ``strands`` is importable; the default workspace venv skips it. Validates the
4
+ thing a fake can't — that ``register_hooks`` subscribes to the *real*
5
+ ``BeforeToolCallEvent`` / ``AfterToolCallEvent`` classes (catches an event
6
+ rename / moved import path). No model/backend needed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import pytest
12
+
13
+ pytest.importorskip("strands")
14
+
15
+ from strands.hooks.events import AfterToolCallEvent, BeforeToolCallEvent # noqa: E402
16
+
17
+ from ory_argus.testing import create_mock_client # noqa: E402
18
+ from ory_strands import OryHooks # noqa: E402
19
+
20
+
21
+ class _RecordingRegistry:
22
+ def __init__(self):
23
+ self.registered: dict = {}
24
+
25
+ def add_callback(self, event_type, callback):
26
+ self.registered[event_type] = callback
27
+
28
+
29
+ def test_register_hooks_subscribes_real_events():
30
+ registry = _RecordingRegistry()
31
+ OryHooks(client=create_mock_client(harness="strands")).register_hooks(registry)
32
+ assert BeforeToolCallEvent in registry.registered
33
+ assert AfterToolCallEvent in registry.registered
34
+ assert callable(registry.registered[BeforeToolCallEvent])
35
+ assert callable(registry.registered[AfterToolCallEvent])