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.
- ory_antigravity-0.13.9/.gitignore +36 -0
- ory_antigravity-0.13.9/PKG-INFO +54 -0
- ory_antigravity-0.13.9/README.md +39 -0
- ory_antigravity-0.13.9/pyproject.toml +23 -0
- ory_antigravity-0.13.9/src/ory_antigravity/__init__.py +16 -0
- ory_antigravity-0.13.9/src/ory_antigravity/hooks.py +179 -0
- ory_antigravity-0.13.9/tests/test_hooks.py +116 -0
- ory_antigravity-0.13.9/tests/test_realsdk.py +75 -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,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()
|