ory-antigravity 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.
@@ -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,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: ory-antigravity
3
+ Version: 0.13.9
4
+ Summary: Ory Agent Security for the Google Antigravity SDK — per-tool authorization, tracing, and identity propagation via the SDK's hook system. Built on ory-argus.
5
+ Author: Ory
6
+ License-Expression: Apache-2.0
7
+ Keywords: agent,ai,antigravity,authorization,gemini,google,ory,permissions
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: google-antigravity>=0.1
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-antigravity
17
+
18
+ Ory Agent Security for the [Google Antigravity SDK](https://antigravity.google/product/antigravity-sdk).
19
+
20
+ Authorizes every tool call against Ory Permissions via Antigravity's hook system, traces
21
+ invocations, and propagates user → agent → sub-agent identity — built on
22
+ [`ory-argus`](https://pypi.org/project/ory-argus/).
23
+
24
+ ```bash
25
+ pip install ory-antigravity
26
+ ```
27
+
28
+ ```python
29
+ from google.antigravity import Agent, LocalAgentConfig
30
+ from ory_antigravity import ory_hooks
31
+
32
+ config = LocalAgentConfig(hooks=ory_hooks(), ...)
33
+ agent = Agent(config)
34
+ ```
35
+
36
+ `ory_hooks()` returns three hooks ready to drop into `config.hooks`:
37
+ `OnSessionStartHook` (auth gates), `PreToolCallDecideHook` (authorize), and
38
+ `PostToolCallHook` (trace). Building your own `HookRunner`? The same object exposes them by
39
+ name: `ory_hooks().pre_tool_call_decide`, `.post_tool_call`, `.on_session_start`.
40
+
41
+ In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a denied tool is **aborted** — the decide
42
+ hook returns `HookResult(allow=False)` and Antigravity hands the model the denial instead of
43
+ running the tool. In **observe** mode (default) the tool runs and a `permission.observe_deny`
44
+ span is recorded. The `ask_question` builtin is treated as a user-interaction primitive (never
45
+ gated); when the model delegates via `start_subagent`, a sub-agent identity and
46
+ agent→subagent delegation are recorded.
47
+
48
+ Antigravity decide hooks fail **closed** (a raising hook aborts the tool), so this integration
49
+ owns fail-open itself: a network or rate-limit error on the permission check resolves to
50
+ *allow*, and an Ory outage never hard-blocks the agent.
51
+
52
+ Credentials come from the shared `~/.config/ory-agent-plugins/config.json`, so a login from
53
+ any Ory harness or `ory-argus login` is reused automatically. See
54
+ [docs/sdk-integrations.md](https://github.com/ory-corp/ory-agent-plugins/blob/main/docs/sdk-integrations.md).
@@ -0,0 +1,39 @@
1
+ # ory-antigravity
2
+
3
+ Ory Agent Security for the [Google Antigravity SDK](https://antigravity.google/product/antigravity-sdk).
4
+
5
+ Authorizes every tool call against Ory Permissions via Antigravity's hook system, traces
6
+ invocations, and propagates user → agent → sub-agent identity — built on
7
+ [`ory-argus`](https://pypi.org/project/ory-argus/).
8
+
9
+ ```bash
10
+ pip install ory-antigravity
11
+ ```
12
+
13
+ ```python
14
+ from google.antigravity import Agent, LocalAgentConfig
15
+ from ory_antigravity import ory_hooks
16
+
17
+ config = LocalAgentConfig(hooks=ory_hooks(), ...)
18
+ agent = Agent(config)
19
+ ```
20
+
21
+ `ory_hooks()` returns three hooks ready to drop into `config.hooks`:
22
+ `OnSessionStartHook` (auth gates), `PreToolCallDecideHook` (authorize), and
23
+ `PostToolCallHook` (trace). Building your own `HookRunner`? The same object exposes them by
24
+ name: `ory_hooks().pre_tool_call_decide`, `.post_tool_call`, `.on_session_start`.
25
+
26
+ In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a denied tool is **aborted** — the decide
27
+ hook returns `HookResult(allow=False)` and Antigravity hands the model the denial instead of
28
+ running the tool. In **observe** mode (default) the tool runs and a `permission.observe_deny`
29
+ span is recorded. The `ask_question` builtin is treated as a user-interaction primitive (never
30
+ gated); when the model delegates via `start_subagent`, a sub-agent identity and
31
+ agent→subagent delegation are recorded.
32
+
33
+ Antigravity decide hooks fail **closed** (a raising hook aborts the tool), so this integration
34
+ owns fail-open itself: a network or rate-limit error on the permission check resolves to
35
+ *allow*, and an Ory outage never hard-blocks the agent.
36
+
37
+ Credentials come from the shared `~/.config/ory-agent-plugins/config.json`, so a login from
38
+ any Ory harness or `ory-argus login` is reused automatically. See
39
+ [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-antigravity"
7
+ version = "0.13.9"
8
+ description = "Ory Agent Security for the Google Antigravity SDK — per-tool authorization, tracing, and identity propagation via the SDK's hook system. 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", "google", "antigravity", "gemini", "authorization", "agent", "ai", "permissions"]
14
+ dependencies = [
15
+ "ory-argus>=0.8,<1",
16
+ "google-antigravity>=0.1",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=8", "ruff>=0.6"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/ory_antigravity"]
@@ -0,0 +1,16 @@
1
+ """Ory Agent Security for the Google Antigravity SDK.
2
+
3
+ from google.antigravity import Agent, LocalAgentConfig
4
+ from ory_antigravity import ory_hooks
5
+
6
+ config = LocalAgentConfig(hooks=ory_hooks(), ...)
7
+ agent = Agent(config)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .hooks import OryHookSet, apply_complete, evaluate, ory_hooks
13
+
14
+ __version__ = "0.13.9"
15
+
16
+ __all__ = ["ory_hooks", "OryHookSet", "evaluate", "apply_complete", "__version__"]
@@ -0,0 +1,179 @@
1
+ """Ory Agent Security for the Google Antigravity SDK.
2
+
3
+ Antigravity exposes a first-class hook system over its agentic loop. The hooks relevant to
4
+ authorization, tracing, and identity are:
5
+
6
+ - ``OnSessionStartHook`` — fires once when the session starts.
7
+ - ``PreToolCallDecideHook`` — a *decide* hook that runs before every tool call and returns
8
+ a ``HookResult(allow=…)``; ``allow=False`` aborts the tool.
9
+ - ``PostToolCallHook`` — an *inspect* hook that fires after a tool completes.
10
+
11
+ :func:`ory_hooks` returns the three hook instances ready to drop into an agent config; they
12
+ authorize each call against Ory Permissions:
13
+
14
+ - allow / observe / fail-open / interactive → ``HookResult(allow=True)`` (the tool runs).
15
+ - deny (enforce) → ``HookResult(allow=False, message=…)`` so Antigravity skips the tool and
16
+ hands the model the denial.
17
+
18
+ from google.antigravity import Agent, LocalAgentConfig
19
+ from ory_antigravity import ory_hooks
20
+
21
+ config = LocalAgentConfig(hooks=ory_hooks(), ...)
22
+ agent = Agent(config)
23
+
24
+ **Fail-open is this integration's responsibility.** Antigravity decide hooks fail *closed* —
25
+ a hook that raises aborts the tool. All gate logic therefore lives behind a top-level catch
26
+ in :func:`evaluate`/:func:`ory_argus.gate` (network / rate-limit errors resolve to ``allow``),
27
+ so an Ory outage never hard-blocks the agent.
28
+
29
+ The gate logic itself lives in ``ory_argus``; the SDK is imported **lazily** inside
30
+ :func:`ory_hooks` (the hook classes must subclass the SDK's abstract bases for the runner's
31
+ ``isinstance`` dispatch), so this module imports fine without ``google-antigravity`` present
32
+ and the decision matrix is unit-testable against the SDK-free helpers below.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ from typing import Any
39
+
40
+ from ory_argus import GateResult, OryAgentClient, complete, gate, register_subagent, session_start
41
+
42
+ HARNESS = "antigravity"
43
+ # Antigravity emits this builtin tool call when the model delegates to a sub-agent.
44
+ _SUBAGENT_TOOL = "start_subagent"
45
+
46
+
47
+ # ─── SDK-free core (unit-tested with duck-typed fakes) ────────────────
48
+
49
+
50
+ def _tool_name(data: Any) -> str:
51
+ """Tool name as a plain string (``BuiltinTools`` is a str-enum → take ``.value``)."""
52
+ name = getattr(data, "name", None)
53
+ return getattr(name, "value", name) or "unknown"
54
+
55
+
56
+ def _tool_args(data: Any) -> Any:
57
+ return getattr(data, "args", None)
58
+
59
+
60
+ def _ensure_session(client: OryAgentClient, project_url: str | None, state: dict) -> None:
61
+ """Run the session-start gates exactly once. Never raises."""
62
+ if state.get("started"):
63
+ return
64
+ state["started"] = True
65
+ try:
66
+ session_start(client, harness=HARNESS, project_url=project_url)
67
+ except Exception as err: # noqa: BLE001 — session gate must never break the agent
68
+ client.logger.warn("session_start.failed", {"message": str(err)})
69
+
70
+
71
+ def evaluate(
72
+ client: OryAgentClient,
73
+ *,
74
+ tool_name: str,
75
+ tool_args: Any = None,
76
+ can_block: bool = True,
77
+ project_url: str | None = None,
78
+ subagent_tool: str | None = _SUBAGENT_TOOL,
79
+ _state: dict | None = None,
80
+ ) -> GateResult:
81
+ """Authorize one tool call. Bootstraps the session, registers sub-agents, runs the gate.
82
+
83
+ Returns a :class:`ory_argus.GateResult`; ``result.blocked`` is True only on a hard deny in
84
+ enforce mode. SDK-free — the Antigravity hook wrapper translates the result into a
85
+ ``HookResult``.
86
+ """
87
+ state = _state if _state is not None else {}
88
+ _ensure_session(client, project_url, state)
89
+
90
+ # Sub-agent delegation: Antigravity signals a handoff via a `start_subagent` tool call.
91
+ if subagent_tool and tool_name == subagent_tool:
92
+ args = tool_args or {}
93
+ target = args.get("name") or args.get("subagent_name")
94
+ if target:
95
+ try:
96
+ register_subagent(client, harness=HARNESS, sub_agent_type=target, project_url=project_url)
97
+ except Exception as err: # noqa: BLE001 — delegation is audit-only, never blocks
98
+ client.logger.warn("subagent.register.failed", {"target": target, "message": str(err)})
99
+
100
+ return gate(client, harness=HARNESS, tool_name=tool_name, tool_args=tool_args, can_block=can_block)
101
+
102
+
103
+ def apply_complete(client: OryAgentClient, *, tool_name: str, result: Any = None) -> None:
104
+ """Record a ``tool.complete`` span for a finished tool call. Never blocks."""
105
+ complete(client, tool_name=tool_name, output=result)
106
+
107
+
108
+ # ─── Public API: SDK hook instances ──────────────────────────────────
109
+
110
+
111
+ class OryHookSet(list):
112
+ """The Ory hook instances, also usable directly as the ``config.hooks`` list.
113
+
114
+ Subclasses ``list`` so it can be passed straight to ``LocalAgentConfig(hooks=…)`` while
115
+ still exposing the individual hooks by name (``.pre_tool_call_decide`` etc.) for callers
116
+ building their own ``HookRunner``.
117
+ """
118
+
119
+ def __init__(self, on_session_start: Any, pre_tool_call_decide: Any, post_tool_call: Any) -> None:
120
+ self.on_session_start = on_session_start
121
+ self.pre_tool_call_decide = pre_tool_call_decide
122
+ self.post_tool_call = post_tool_call
123
+ super().__init__([on_session_start, pre_tool_call_decide, post_tool_call])
124
+
125
+
126
+ def ory_hooks(
127
+ *,
128
+ client: OryAgentClient | None = None,
129
+ can_block: bool = True,
130
+ project_url: str | None = None,
131
+ subagent_tool: str | None = _SUBAGENT_TOOL,
132
+ ) -> OryHookSet:
133
+ """Build the Ory Antigravity hooks. Drop the result into ``config.hooks``.
134
+
135
+ Args:
136
+ client: An ``OryAgentClient``. Defaults to ``OryAgentClient.from_env("antigravity")``.
137
+ can_block: Whether a denied tool is hard-aborted (sets the alert attribute). Default ``True``.
138
+ project_url: Override the Ory project URL for the session gates.
139
+ subagent_tool: Builtin tool name that signals a sub-agent handoff. ``None`` disables
140
+ sub-agent registration. Default ``"start_subagent"``.
141
+ """
142
+ from google.antigravity.hooks import OnSessionStartHook, PostToolCallHook, PreToolCallDecideHook
143
+ from google.antigravity.types import HookResult
144
+
145
+ c = client or OryAgentClient.from_env(HARNESS)
146
+ state: dict = {}
147
+
148
+ class _OnSessionStart(OnSessionStartHook):
149
+ async def run(self, context: Any, data: Any = None) -> None:
150
+ await asyncio.to_thread(_ensure_session, c, project_url, state)
151
+
152
+ class _PreToolCallDecide(PreToolCallDecideHook):
153
+ async def run(self, context: Any, data: Any) -> HookResult:
154
+ result = await asyncio.to_thread(
155
+ evaluate,
156
+ c,
157
+ tool_name=_tool_name(data),
158
+ tool_args=_tool_args(data),
159
+ can_block=can_block,
160
+ project_url=project_url,
161
+ subagent_tool=subagent_tool,
162
+ _state=state,
163
+ )
164
+ return HookResult(allow=not result.blocked, message=result.denial_message or "")
165
+
166
+ class _PostToolCall(PostToolCallHook):
167
+ async def run(self, context: Any, data: Any) -> None:
168
+ apply_complete(c, tool_name=_tool_name(data), result=getattr(data, "result", None))
169
+
170
+ return OryHookSet(_OnSessionStart(), _PreToolCallDecide(), _PostToolCall())
171
+
172
+
173
+ __all__ = [
174
+ "HARNESS",
175
+ "OryHookSet",
176
+ "ory_hooks",
177
+ "evaluate",
178
+ "apply_complete",
179
+ ]
@@ -0,0 +1,116 @@
1
+ """Hermetic tests for the Antigravity gate logic (mocked Ory, duck-typed ToolCall/ToolResult).
2
+
3
+ These exercise the SDK-free core (:func:`evaluate` / :func:`apply_complete`) without
4
+ importing ``google-antigravity``: the blocked/denial-message veto surface that the async
5
+ hooks translate into a ``HookResult`` (covered by ``test_realsdk.py``), plus the
6
+ package-specific paths — the ``ask_question`` interactive built-in, str-enum tool-name
7
+ normalization, ``start_subagent`` delegation, and session-once. The full permission decision
8
+ matrix is core-owned (``ory-argus/tests/test_adapters.py``).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from types import SimpleNamespace
14
+
15
+ import pytest
16
+
17
+ from ory_antigravity import apply_complete, evaluate
18
+ from ory_antigravity import hooks as hooks_mod
19
+ from ory_argus.client import PrincipalIdentity
20
+ from ory_argus.testing import (
21
+ create_mock_client,
22
+ get_trace_spans,
23
+ stub_permission_allowed,
24
+ stub_permission_denied,
25
+ )
26
+
27
+
28
+ @pytest.fixture(autouse=True)
29
+ def _isolate(monkeypatch, tmp_path):
30
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
31
+ monkeypatch.delenv("ORY_PERMISSION_MODE", raising=False)
32
+ monkeypatch.delenv("ORY_INTERACTIVE_TOOLS", raising=False)
33
+ monkeypatch.setattr(hooks_mod, "session_start", lambda *a, **k: None)
34
+
35
+
36
+ def client_with_user():
37
+ c = create_mock_client(harness="antigravity")
38
+ c.set_user_principal(PrincipalIdentity(subject="user:alice"))
39
+ return c
40
+
41
+
42
+ def _tool_call(name="search_web", args=None):
43
+ return SimpleNamespace(name=name, args=args if args is not None else {"q": 1})
44
+
45
+
46
+ def test_allow_proceeds():
47
+ c = client_with_user()
48
+ stub_permission_allowed(c)
49
+ result = evaluate(c, tool_name="search_web", tool_args={"q": 1})
50
+ assert result.blocked is False
51
+ invoke = get_trace_spans(c, "tool.invoke")[0]
52
+ assert invoke.attributes["allowed"] is True
53
+ assert invoke.attributes["toolName"] == "search_web"
54
+
55
+
56
+ def test_enforce_blocks(monkeypatch):
57
+ monkeypatch.setenv("ORY_PERMISSION_MODE", "enforce")
58
+ c = client_with_user()
59
+ stub_permission_denied(c)
60
+ result = evaluate(c, tool_name="run_command", tool_args={"cmd": "rm -rf /"})
61
+ # Native veto shape: blocked + denial_message, which the async hook translates to
62
+ # Antigravity's ``{allow_tool: False, deny_reason}`` HookResult (see test_realsdk.py).
63
+ assert result.blocked is True
64
+ assert "permission denied" in (result.denial_message or "").lower()
65
+ assert get_trace_spans(c, "tool.block")[0].attributes["blocked"] is True
66
+
67
+
68
+ def test_ask_question_is_interactive():
69
+ c = client_with_user()
70
+ stub_permission_denied(c) # even a deny must not gate the user-interaction primitive
71
+ result = evaluate(c, tool_name="ask_question", tool_args={"prompt": "which file?"})
72
+ assert result.blocked is False
73
+ assert result.kind == "interactive"
74
+ assert get_trace_spans(c, "user.interaction")[0].attributes["toolName"] == "ask_question"
75
+
76
+
77
+ def test_builtin_tools_enum_name_resolves(monkeypatch):
78
+ """A str-enum tool name (BuiltinTools) is normalized to its ``.value``."""
79
+ c = client_with_user()
80
+ stub_permission_allowed(c)
81
+ enum_name = SimpleNamespace(value="search_web")
82
+ result = evaluate(c, tool_name=hooks_mod._tool_name(_tool_call(name=enum_name)))
83
+ assert result.blocked is False
84
+ assert get_trace_spans(c, "tool.invoke")[0].attributes["toolName"] == "search_web"
85
+
86
+
87
+ def test_start_subagent_registers_delegation(monkeypatch):
88
+ c = client_with_user()
89
+ stub_permission_allowed(c)
90
+ seen = {}
91
+ monkeypatch.setattr(
92
+ hooks_mod, "register_subagent",
93
+ lambda client, **kw: seen.update(kw),
94
+ )
95
+ result = evaluate(c, tool_name="start_subagent", tool_args={"name": "researcher"})
96
+ assert seen["sub_agent_type"] == "researcher"
97
+ assert seen["harness"] == "antigravity"
98
+ assert result.blocked is False # the start_subagent call itself is still gated + allowed
99
+
100
+
101
+ def test_session_start_runs_once(monkeypatch):
102
+ c = client_with_user()
103
+ stub_permission_allowed(c)
104
+ calls = []
105
+ monkeypatch.setattr(hooks_mod, "session_start", lambda *a, **k: calls.append(1))
106
+ state: dict = {}
107
+ evaluate(c, tool_name="search_web", _state=state)
108
+ evaluate(c, tool_name="view_file", _state=state)
109
+ assert calls == [1] # session_start fired exactly once across both tool calls
110
+
111
+
112
+ def test_after_records_complete():
113
+ c = client_with_user()
114
+ apply_complete(c, tool_name="search_web", result=SimpleNamespace(result="ok"))
115
+ spans = get_trace_spans(c, "tool.complete")
116
+ assert spans[0].attributes["toolName"] == "search_web"
@@ -0,0 +1,75 @@
1
+ """Real-SDK test: the Ory hooks subclass the real Antigravity bases and translate decisions.
2
+
3
+ Runs only when ``google-antigravity`` is importable; the default workspace venv skips it.
4
+ Validates what a duck-typed fake can't: (1) the hooks are real ``OnSessionStartHook`` /
5
+ ``PreToolCallDecideHook`` / ``PostToolCallHook`` subclasses, so the SDK's ``HookRunner``
6
+ ``isinstance`` dispatch accepts them, and (2) the decide hook returns a real
7
+ ``HookResult`` whose ``allow`` flips with the gate decision. The permission check is stubbed,
8
+ so no model/backend is needed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+
15
+ import pytest
16
+
17
+ pytest.importorskip("google.antigravity")
18
+
19
+ from google.antigravity.hooks import ( # noqa: E402
20
+ OnSessionStartHook,
21
+ PostToolCallHook,
22
+ PreToolCallDecideHook,
23
+ )
24
+ from google.antigravity.hooks.hook_runner import HookRunner # noqa: E402
25
+ from google.antigravity.types import HookResult, ToolCall # noqa: E402
26
+
27
+ from ory_antigravity import hooks as hooks_mod # noqa: E402
28
+ from ory_antigravity import ory_hooks # noqa: E402
29
+ from ory_argus.client import PrincipalIdentity # noqa: E402
30
+ from ory_argus.testing import ( # noqa: E402
31
+ create_mock_client,
32
+ stub_permission_allowed,
33
+ stub_permission_denied,
34
+ )
35
+
36
+
37
+ def _client(monkeypatch):
38
+ monkeypatch.setattr(hooks_mod, "session_start", lambda *a, **k: None)
39
+ c = create_mock_client(harness="antigravity")
40
+ c.set_user_principal(PrincipalIdentity(subject="user:alice"))
41
+ return c
42
+
43
+
44
+ def test_hooks_subclass_real_bases_and_register(monkeypatch):
45
+ hs = ory_hooks(client=_client(monkeypatch))
46
+ assert isinstance(hs.on_session_start, OnSessionStartHook)
47
+ assert isinstance(hs.pre_tool_call_decide, PreToolCallDecideHook)
48
+ assert isinstance(hs.post_tool_call, PostToolCallHook)
49
+ # The runner's isinstance dispatch must accept all three without raising.
50
+ runner = HookRunner()
51
+ for hook in hs:
52
+ runner.register_hook(hook)
53
+ assert hs.pre_tool_call_decide in runner.pre_tool_call_decide_hooks
54
+ assert hs.post_tool_call in runner.post_tool_call_hooks
55
+ assert hs.on_session_start in runner.on_session_start_hooks
56
+
57
+
58
+ def test_decide_allows_returns_hookresult(monkeypatch):
59
+ c = _client(monkeypatch)
60
+ stub_permission_allowed(c)
61
+ hs = ory_hooks(client=c)
62
+ result = asyncio.run(hs.pre_tool_call_decide.run(None, ToolCall(name="search_web", args={"q": 1})))
63
+ assert isinstance(result, HookResult)
64
+ assert result.allow is True
65
+
66
+
67
+ def test_decide_denies_in_enforce(monkeypatch):
68
+ monkeypatch.setenv("ORY_PERMISSION_MODE", "enforce")
69
+ c = _client(monkeypatch)
70
+ stub_permission_denied(c)
71
+ hs = ory_hooks(client=c)
72
+ result = asyncio.run(hs.pre_tool_call_decide.run(None, ToolCall(name="run_command", args={"cmd": "rm"})))
73
+ assert isinstance(result, HookResult)
74
+ assert result.allow is False
75
+ assert "permission denied" in result.message.lower()