ory-argus 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_argus-0.13.9/.gitignore +36 -0
- ory_argus-0.13.9/PKG-INFO +42 -0
- ory_argus-0.13.9/README.md +23 -0
- ory_argus-0.13.9/pyproject.toml +29 -0
- ory_argus-0.13.9/src/ory_argus/__init__.py +238 -0
- ory_argus-0.13.9/src/ory_argus/adapters.py +308 -0
- ory_argus-0.13.9/src/ory_argus/agent_auth.py +576 -0
- ory_argus-0.13.9/src/ory_argus/auth.py +328 -0
- ory_argus-0.13.9/src/ory_argus/auth_store.py +171 -0
- ory_argus-0.13.9/src/ory_argus/cli.py +608 -0
- ory_argus-0.13.9/src/ory_argus/client.py +604 -0
- ory_argus-0.13.9/src/ory_argus/config.py +571 -0
- ory_argus-0.13.9/src/ory_argus/denial.py +108 -0
- ory_argus-0.13.9/src/ory_argus/lifecycle.py +216 -0
- ory_argus-0.13.9/src/ory_argus/logger.py +108 -0
- ory_argus-0.13.9/src/ory_argus/mcp.py +14 -0
- ory_argus-0.13.9/src/ory_argus/otel.py +368 -0
- ory_argus-0.13.9/src/ory_argus/permissions.py +231 -0
- ory_argus-0.13.9/src/ory_argus/subject.py +79 -0
- ory_argus-0.13.9/src/ory_argus/testing.py +188 -0
- ory_argus-0.13.9/src/ory_argus/tool_catalog.py +107 -0
- ory_argus-0.13.9/src/ory_argus/tracer.py +498 -0
- ory_argus-0.13.9/src/ory_argus/types.py +130 -0
- ory_argus-0.13.9/src/ory_argus/user_login.py +266 -0
- ory_argus-0.13.9/tests/test_adapters.py +265 -0
- ory_argus-0.13.9/tests/test_auth.py +1114 -0
- ory_argus-0.13.9/tests/test_cli.py +434 -0
- ory_argus-0.13.9/tests/test_client.py +452 -0
- ory_argus-0.13.9/tests/test_config.py +268 -0
- ory_argus-0.13.9/tests/test_e2e.py +85 -0
- ory_argus-0.13.9/tests/test_lifecycle.py +71 -0
- ory_argus-0.13.9/tests/test_permissions.py +250 -0
- ory_argus-0.13.9/tests/test_tracer.py +138 -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,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ory-argus
|
|
3
|
+
Version: 0.13.9
|
|
4
|
+
Summary: Ory Agent Security core for Python — authentication, per-tool authorization, tracing, and identity propagation for Agent SDK integrations. Python sibling of the TypeScript @ory/argus.
|
|
5
|
+
Author: Ory
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: agent,ai,authentication,authorization,ory,permissions,tracing
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.24
|
|
10
|
+
Requires-Dist: opentelemetry-api>=1.20
|
|
11
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20
|
|
12
|
+
Requires-Dist: opentelemetry-sdk>=1.20
|
|
13
|
+
Requires-Dist: ory-client<2,>=1.22
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# ory-argus (Python)
|
|
21
|
+
|
|
22
|
+
Ory Agent Security core for Python — the sibling of the TypeScript
|
|
23
|
+
[`@ory/argus`](https://www.npmjs.com/package/@ory/argus) package.
|
|
24
|
+
|
|
25
|
+
It provides the shared primitives that the Ory **Agent SDK integrations**
|
|
26
|
+
(LangChain, OpenAI Agents, Pydantic AI, Microsoft Agent Framework, CrewAI,
|
|
27
|
+
LlamaIndex, Google ADK, AWS Strands, Google Antigravity) build on:
|
|
28
|
+
|
|
29
|
+
1. **Authenticate** the session on startup (user PKCE login + agent OAuth2 identity).
|
|
30
|
+
2. **Authorize** every tool call against Ory Permissions (Zanzibar-style relation checks).
|
|
31
|
+
3. **Trace** every tool invocation as a structured span (OTLP + NDJSON).
|
|
32
|
+
4. **Propagate identity** through the user → agent → sub-agent delegation chain.
|
|
33
|
+
|
|
34
|
+
## Interop with the TypeScript core
|
|
35
|
+
|
|
36
|
+
`ory-argus` reads and writes the **same** shared config file as every TS harness
|
|
37
|
+
plugin — `~/.config/ory-agent-plugins/config.json` (XDG / `%APPDATA%` aware). A
|
|
38
|
+
user who logs in via a TS coding-agent harness has those credentials transparently
|
|
39
|
+
reused by a Python SDK integration, and vice-versa. This package never invents a
|
|
40
|
+
second config location; use `get_data_dir()` / `get_config_path()`.
|
|
41
|
+
|
|
42
|
+
Install: `pip install ory-argus` · Console script: `ory-argus`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ory-argus (Python)
|
|
2
|
+
|
|
3
|
+
Ory Agent Security core for Python — the sibling of the TypeScript
|
|
4
|
+
[`@ory/argus`](https://www.npmjs.com/package/@ory/argus) package.
|
|
5
|
+
|
|
6
|
+
It provides the shared primitives that the Ory **Agent SDK integrations**
|
|
7
|
+
(LangChain, OpenAI Agents, Pydantic AI, Microsoft Agent Framework, CrewAI,
|
|
8
|
+
LlamaIndex, Google ADK, AWS Strands, Google Antigravity) build on:
|
|
9
|
+
|
|
10
|
+
1. **Authenticate** the session on startup (user PKCE login + agent OAuth2 identity).
|
|
11
|
+
2. **Authorize** every tool call against Ory Permissions (Zanzibar-style relation checks).
|
|
12
|
+
3. **Trace** every tool invocation as a structured span (OTLP + NDJSON).
|
|
13
|
+
4. **Propagate identity** through the user → agent → sub-agent delegation chain.
|
|
14
|
+
|
|
15
|
+
## Interop with the TypeScript core
|
|
16
|
+
|
|
17
|
+
`ory-argus` reads and writes the **same** shared config file as every TS harness
|
|
18
|
+
plugin — `~/.config/ory-agent-plugins/config.json` (XDG / `%APPDATA%` aware). A
|
|
19
|
+
user who logs in via a TS coding-agent harness has those credentials transparently
|
|
20
|
+
reused by a Python SDK integration, and vice-versa. This package never invents a
|
|
21
|
+
second config location; use `get_data_dir()` / `get_config_path()`.
|
|
22
|
+
|
|
23
|
+
Install: `pip install ory-argus` · Console script: `ory-argus`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ory-argus"
|
|
7
|
+
version = "0.13.9"
|
|
8
|
+
description = "Ory Agent Security core for Python — authentication, per-tool authorization, tracing, and identity propagation for Agent SDK integrations. Python sibling of the TypeScript @ory/argus."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Ory" }]
|
|
13
|
+
keywords = ["ory", "authorization", "authentication", "agent", "ai", "tracing", "permissions"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"ory-client>=1.22,<2",
|
|
16
|
+
"opentelemetry-api>=1.20",
|
|
17
|
+
"opentelemetry-sdk>=1.20",
|
|
18
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20",
|
|
19
|
+
"httpx>=0.24",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.6"]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
ory-argus = "ory_argus.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/ory_argus"]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Ory Agent Security core for Python.
|
|
2
|
+
|
|
3
|
+
Python sibling of the TypeScript ``@ory/argus`` core. Provides the same
|
|
4
|
+
authentication, per-tool authorization, tracing, and identity-propagation
|
|
5
|
+
primitives, reading and writing the *same* shared config file so credentials
|
|
6
|
+
established in one ecosystem are reusable from the other.
|
|
7
|
+
|
|
8
|
+
Public API is re-exported from the submodules as they land. ``config`` is the
|
|
9
|
+
cross-language interop contract and is available now.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .adapters import (
|
|
15
|
+
GateResult,
|
|
16
|
+
SessionStartResult,
|
|
17
|
+
complete,
|
|
18
|
+
gate,
|
|
19
|
+
guarded_tool,
|
|
20
|
+
register_subagent,
|
|
21
|
+
resolve_namespace,
|
|
22
|
+
session_start,
|
|
23
|
+
)
|
|
24
|
+
from .agent_auth import (
|
|
25
|
+
AgentCredentials,
|
|
26
|
+
SubAgentIdentity,
|
|
27
|
+
ensure_agent_identity,
|
|
28
|
+
ensure_subagent_identity,
|
|
29
|
+
register_agent_client,
|
|
30
|
+
resolve_agent_credentials,
|
|
31
|
+
)
|
|
32
|
+
from .auth import (
|
|
33
|
+
DEFAULT_LOGIN_TIMEOUT_MS,
|
|
34
|
+
LOOPBACK_PORTS,
|
|
35
|
+
PkceLoginOutcome,
|
|
36
|
+
detect_headless,
|
|
37
|
+
pkce_login,
|
|
38
|
+
refresh_access_token,
|
|
39
|
+
)
|
|
40
|
+
from .auth_store import (
|
|
41
|
+
clear_tokens,
|
|
42
|
+
is_expired,
|
|
43
|
+
load_tokens,
|
|
44
|
+
save_tokens,
|
|
45
|
+
)
|
|
46
|
+
from .client import OryAgentClient, PrincipalIdentity, extract_ory_request_id
|
|
47
|
+
from .config import (
|
|
48
|
+
DELETE,
|
|
49
|
+
OryAgentCredentialsBlock,
|
|
50
|
+
OryAgentDynamicCredentials,
|
|
51
|
+
OryOAuth2Tokens,
|
|
52
|
+
OryPluginConfig,
|
|
53
|
+
OryUserCredentials,
|
|
54
|
+
PermissionMode,
|
|
55
|
+
ResolvedConfig,
|
|
56
|
+
config_prompt_message,
|
|
57
|
+
get_config_path,
|
|
58
|
+
get_data_dir,
|
|
59
|
+
get_harness_data_dir,
|
|
60
|
+
load_config,
|
|
61
|
+
mutate_config,
|
|
62
|
+
resolve_config,
|
|
63
|
+
save_config,
|
|
64
|
+
)
|
|
65
|
+
from .denial import (
|
|
66
|
+
DenialContext,
|
|
67
|
+
OryDenialError,
|
|
68
|
+
alert_attributes,
|
|
69
|
+
format_alert_message,
|
|
70
|
+
format_denial_message,
|
|
71
|
+
format_denial_summary,
|
|
72
|
+
)
|
|
73
|
+
from .lifecycle import (
|
|
74
|
+
HARNESS_LIFECYCLE_MAP,
|
|
75
|
+
LifecyclePhase,
|
|
76
|
+
classify_lifecycle,
|
|
77
|
+
is_tool_execution_phase,
|
|
78
|
+
is_user_facing_phase,
|
|
79
|
+
)
|
|
80
|
+
from .logger import DebugLogger, redact_log_data
|
|
81
|
+
from .otel import OtlpExporter, SpanExporter, otlp_exporter_from_env
|
|
82
|
+
from .permissions import (
|
|
83
|
+
ModeDecision,
|
|
84
|
+
PermissionDecision,
|
|
85
|
+
apply_permission_mode,
|
|
86
|
+
check_and_decide,
|
|
87
|
+
gate_tool_call,
|
|
88
|
+
)
|
|
89
|
+
from .subject import (
|
|
90
|
+
SubjectId,
|
|
91
|
+
SubjectSetRef,
|
|
92
|
+
UserSubjectRef,
|
|
93
|
+
apply_subject_to_check_kwargs,
|
|
94
|
+
resolve_user_subject,
|
|
95
|
+
subject_label,
|
|
96
|
+
)
|
|
97
|
+
from .tool_catalog import (
|
|
98
|
+
ALL_TOOLS,
|
|
99
|
+
HARNESS_TOOL_CATALOG,
|
|
100
|
+
INTERACTIVE_TOOL_CATALOG,
|
|
101
|
+
KNOWN_HARNESSES,
|
|
102
|
+
get_interactive_tool_catalog,
|
|
103
|
+
get_tool_catalog,
|
|
104
|
+
is_interactive_tool,
|
|
105
|
+
)
|
|
106
|
+
from .tracer import (
|
|
107
|
+
ActiveSpan,
|
|
108
|
+
SpanOptions,
|
|
109
|
+
Tracer,
|
|
110
|
+
TracerContext,
|
|
111
|
+
TraceSpan,
|
|
112
|
+
derive_trace_id,
|
|
113
|
+
format_span,
|
|
114
|
+
)
|
|
115
|
+
from .types import (
|
|
116
|
+
BatchPermissionResult,
|
|
117
|
+
OAuth2TokenInfo,
|
|
118
|
+
OryError,
|
|
119
|
+
OryErrorCode,
|
|
120
|
+
PermissionCheck,
|
|
121
|
+
PermissionResult,
|
|
122
|
+
SessionInfo,
|
|
123
|
+
SubjectSet,
|
|
124
|
+
)
|
|
125
|
+
from .user_login import UserLoginDecision, ensure_user_authenticated
|
|
126
|
+
|
|
127
|
+
__version__ = "0.13.9"
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"__version__",
|
|
131
|
+
# config
|
|
132
|
+
"DELETE",
|
|
133
|
+
"PermissionMode",
|
|
134
|
+
"OryOAuth2Tokens",
|
|
135
|
+
"OryUserCredentials",
|
|
136
|
+
"OryAgentDynamicCredentials",
|
|
137
|
+
"OryAgentCredentialsBlock",
|
|
138
|
+
"OryPluginConfig",
|
|
139
|
+
"ResolvedConfig",
|
|
140
|
+
"get_data_dir",
|
|
141
|
+
"get_harness_data_dir",
|
|
142
|
+
"get_config_path",
|
|
143
|
+
"load_config",
|
|
144
|
+
"save_config",
|
|
145
|
+
"mutate_config",
|
|
146
|
+
"resolve_config",
|
|
147
|
+
"config_prompt_message",
|
|
148
|
+
# client
|
|
149
|
+
"OryAgentClient",
|
|
150
|
+
"PrincipalIdentity",
|
|
151
|
+
"extract_ory_request_id",
|
|
152
|
+
# logger
|
|
153
|
+
"DebugLogger",
|
|
154
|
+
"redact_log_data",
|
|
155
|
+
# tracer / otel
|
|
156
|
+
"Tracer",
|
|
157
|
+
"TraceSpan",
|
|
158
|
+
"ActiveSpan",
|
|
159
|
+
"SpanOptions",
|
|
160
|
+
"TracerContext",
|
|
161
|
+
"derive_trace_id",
|
|
162
|
+
"format_span",
|
|
163
|
+
"SpanExporter",
|
|
164
|
+
"OtlpExporter",
|
|
165
|
+
"otlp_exporter_from_env",
|
|
166
|
+
# types
|
|
167
|
+
"SessionInfo",
|
|
168
|
+
"OAuth2TokenInfo",
|
|
169
|
+
"PermissionCheck",
|
|
170
|
+
"SubjectSet",
|
|
171
|
+
"PermissionResult",
|
|
172
|
+
"BatchPermissionResult",
|
|
173
|
+
"OryError",
|
|
174
|
+
"OryErrorCode",
|
|
175
|
+
# permissions
|
|
176
|
+
"PermissionDecision",
|
|
177
|
+
"ModeDecision",
|
|
178
|
+
"gate_tool_call",
|
|
179
|
+
"check_and_decide",
|
|
180
|
+
"apply_permission_mode",
|
|
181
|
+
# subject
|
|
182
|
+
"UserSubjectRef",
|
|
183
|
+
"SubjectId",
|
|
184
|
+
"SubjectSetRef",
|
|
185
|
+
"resolve_user_subject",
|
|
186
|
+
"subject_label",
|
|
187
|
+
"apply_subject_to_check_kwargs",
|
|
188
|
+
# denial
|
|
189
|
+
"DenialContext",
|
|
190
|
+
"OryDenialError",
|
|
191
|
+
"alert_attributes",
|
|
192
|
+
"format_denial_message",
|
|
193
|
+
"format_denial_summary",
|
|
194
|
+
"format_alert_message",
|
|
195
|
+
# tool catalog
|
|
196
|
+
"HARNESS_TOOL_CATALOG",
|
|
197
|
+
"INTERACTIVE_TOOL_CATALOG",
|
|
198
|
+
"KNOWN_HARNESSES",
|
|
199
|
+
"ALL_TOOLS",
|
|
200
|
+
"get_tool_catalog",
|
|
201
|
+
"get_interactive_tool_catalog",
|
|
202
|
+
"is_interactive_tool",
|
|
203
|
+
# lifecycle
|
|
204
|
+
"LifecyclePhase",
|
|
205
|
+
"HARNESS_LIFECYCLE_MAP",
|
|
206
|
+
"classify_lifecycle",
|
|
207
|
+
"is_user_facing_phase",
|
|
208
|
+
"is_tool_execution_phase",
|
|
209
|
+
# auth — user
|
|
210
|
+
"ensure_user_authenticated",
|
|
211
|
+
"UserLoginDecision",
|
|
212
|
+
"pkce_login",
|
|
213
|
+
"PkceLoginOutcome",
|
|
214
|
+
"refresh_access_token",
|
|
215
|
+
"detect_headless",
|
|
216
|
+
"LOOPBACK_PORTS",
|
|
217
|
+
"DEFAULT_LOGIN_TIMEOUT_MS",
|
|
218
|
+
"load_tokens",
|
|
219
|
+
"save_tokens",
|
|
220
|
+
"clear_tokens",
|
|
221
|
+
"is_expired",
|
|
222
|
+
# auth — agent
|
|
223
|
+
"ensure_agent_identity",
|
|
224
|
+
"ensure_subagent_identity",
|
|
225
|
+
"resolve_agent_credentials",
|
|
226
|
+
"register_agent_client",
|
|
227
|
+
"AgentCredentials",
|
|
228
|
+
"SubAgentIdentity",
|
|
229
|
+
# adapters (the thin-integration surface)
|
|
230
|
+
"session_start",
|
|
231
|
+
"gate",
|
|
232
|
+
"complete",
|
|
233
|
+
"register_subagent",
|
|
234
|
+
"guarded_tool",
|
|
235
|
+
"resolve_namespace",
|
|
236
|
+
"SessionStartResult",
|
|
237
|
+
"GateResult",
|
|
238
|
+
]
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Shared adapter primitives for Agent SDK integrations.
|
|
2
|
+
|
|
3
|
+
Every SDK integration is a thin translation between the SDK's hook signature and these
|
|
4
|
+
primitives, so the span boilerplate and the session/sub-agent sequencing live in exactly
|
|
5
|
+
one place (the Python sibling of the TS ``packages/core/src/adapters/`` extraction):
|
|
6
|
+
|
|
7
|
+
- :func:`session_start` — run the user + agent auth gates and write the user→agent
|
|
8
|
+
delegation tuple. Returns whether the session may proceed.
|
|
9
|
+
- :func:`gate` — resolve the subject, run :func:`gate_tool_call`, record the
|
|
10
|
+
``tool.invoke`` / ``tool.block`` spans, and return a normalized :class:`GateResult`.
|
|
11
|
+
- :func:`complete` — record the ``tool.complete`` span.
|
|
12
|
+
- :func:`register_subagent` — resolve a sub-agent identity and write the
|
|
13
|
+
agent→subagent delegation tuple.
|
|
14
|
+
- :func:`guarded_tool` — wrap a plain ``execute(args)`` callable with gate+complete, for
|
|
15
|
+
SDKs whose only veto point is the tool boundary (CrewAI, LlamaIndex, Vercel).
|
|
16
|
+
|
|
17
|
+
Every primitive is best-effort and fail-open: identity/delegation failures are logged and
|
|
18
|
+
swallowed, and a network/rate-limit error on a permission check lets the tool proceed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
from .agent_auth import ensure_agent_identity, ensure_subagent_identity
|
|
29
|
+
from .config import resolve_config
|
|
30
|
+
from .denial import DenialContext, OryDenialError, alert_attributes, format_denial_message
|
|
31
|
+
from .permissions import PermissionDecision, gate_tool_call
|
|
32
|
+
from .subject import apply_subject_to_check_kwargs, resolve_user_subject, subject_label
|
|
33
|
+
from .types import PermissionCheck
|
|
34
|
+
from .user_login import ensure_user_authenticated
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from .client import OryAgentClient
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_namespace() -> str:
|
|
41
|
+
return os.environ.get("ORY_PERMISSION_NAMESPACE") or "AgentTools"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ─── Session start ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SessionStartResult:
|
|
49
|
+
proceed: bool
|
|
50
|
+
user_mode: str
|
|
51
|
+
user_reason: str
|
|
52
|
+
agent_kind: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def session_start(
|
|
56
|
+
client: OryAgentClient,
|
|
57
|
+
*,
|
|
58
|
+
harness: str,
|
|
59
|
+
allow_block: bool = False,
|
|
60
|
+
project_url: str | None = None,
|
|
61
|
+
bin_name: str | None = None,
|
|
62
|
+
user_login: Callable[..., Any] = ensure_user_authenticated,
|
|
63
|
+
agent_gate: Callable[..., Any] = ensure_agent_identity,
|
|
64
|
+
) -> SessionStartResult:
|
|
65
|
+
"""Run both auth gates and write the user→agent delegation tuple.
|
|
66
|
+
|
|
67
|
+
``allow_block`` is advisory for most SDK integrations (an agent constructed in-process
|
|
68
|
+
can't be prevented from starting), but the user gate still runs, refreshes tokens, and
|
|
69
|
+
records the ``user.auth`` span. The agent gate and delegation write always run.
|
|
70
|
+
"""
|
|
71
|
+
resolved_url = project_url or resolve_config().project_url
|
|
72
|
+
decision = user_login(client, bin_name=bin_name or f"ory-{harness}", harness=harness, allow_block=allow_block)
|
|
73
|
+
creds = agent_gate(client, project_url=resolved_url, harness=harness)
|
|
74
|
+
_record_user_delegates_agent(client)
|
|
75
|
+
return SessionStartResult(
|
|
76
|
+
proceed=getattr(decision, "proceed", True),
|
|
77
|
+
user_mode=getattr(decision, "mode", "disabled"),
|
|
78
|
+
user_reason=getattr(decision, "reason", ""),
|
|
79
|
+
agent_kind=getattr(creds, "kind", "none"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _record_user_delegates_agent(client: OryAgentClient) -> None:
|
|
84
|
+
user = client.user_principal.subject
|
|
85
|
+
agent = client.agent_principal.subject
|
|
86
|
+
if not user or not agent:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
client.create_relationship(
|
|
90
|
+
PermissionCheck(
|
|
91
|
+
namespace=resolve_namespace(),
|
|
92
|
+
object=f"agent:{agent}",
|
|
93
|
+
relation="delegate",
|
|
94
|
+
subject_id=f"user:{user}",
|
|
95
|
+
),
|
|
96
|
+
span_attributes={"delegation": "user-to-agent"},
|
|
97
|
+
)
|
|
98
|
+
except Exception as err: # noqa: BLE001 — delegation tuples are audit-only
|
|
99
|
+
client.logger.warn("delegation.user_to_agent.failed", {"message": str(err)})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ─── Sub-agent registration ───────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def register_subagent(
|
|
106
|
+
client: OryAgentClient,
|
|
107
|
+
*,
|
|
108
|
+
harness: str,
|
|
109
|
+
sub_agent_type: str,
|
|
110
|
+
project_url: str | None = None,
|
|
111
|
+
subagent_gate: Callable[..., Any] = ensure_subagent_identity,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Resolve a sub-agent identity and write the agent→subagent delegation tuple."""
|
|
114
|
+
resolved_url = project_url or resolve_config().project_url
|
|
115
|
+
try:
|
|
116
|
+
identity = subagent_gate(client, sub_agent_type=sub_agent_type, project_url=resolved_url, harness=harness)
|
|
117
|
+
except Exception as err: # noqa: BLE001
|
|
118
|
+
client.logger.warn("subagent.identity.failed", {"subAgentType": sub_agent_type, "message": str(err)})
|
|
119
|
+
return
|
|
120
|
+
if getattr(identity, "kind", "none") != "dynamic" or not identity.subject:
|
|
121
|
+
return
|
|
122
|
+
agent = client.agent_principal.subject
|
|
123
|
+
if not agent:
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
client.create_relationship(
|
|
127
|
+
PermissionCheck(
|
|
128
|
+
namespace=resolve_namespace(),
|
|
129
|
+
object=f"subagent:{identity.subject}",
|
|
130
|
+
relation="delegate",
|
|
131
|
+
subject_id=f"agent:{agent}",
|
|
132
|
+
),
|
|
133
|
+
span_attributes={"delegation": "agent-to-subagent", "subAgentType": sub_agent_type},
|
|
134
|
+
)
|
|
135
|
+
except Exception as err: # noqa: BLE001
|
|
136
|
+
client.logger.warn("delegation.agent_to_subagent.failed", {"subAgentType": sub_agent_type, "message": str(err)})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ─── Tool gate ────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class GateResult:
|
|
144
|
+
"""Normalized outcome of :func:`gate`.
|
|
145
|
+
|
|
146
|
+
``proceed`` is True unless the tool was hard-denied in enforce mode. ``blocked`` is True
|
|
147
|
+
only for that deny. ``decision`` carries the raw :class:`PermissionDecision`.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
proceed: bool
|
|
151
|
+
blocked: bool
|
|
152
|
+
decision: PermissionDecision
|
|
153
|
+
subject: str
|
|
154
|
+
namespace: str
|
|
155
|
+
denial_message: str | None = None
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def kind(self) -> str:
|
|
159
|
+
return self.decision.kind
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def gate(
|
|
163
|
+
client: OryAgentClient,
|
|
164
|
+
*,
|
|
165
|
+
harness: str,
|
|
166
|
+
tool_name: str,
|
|
167
|
+
tool_args: Any = None,
|
|
168
|
+
subject_fallback: str | None = None,
|
|
169
|
+
can_block: bool = True,
|
|
170
|
+
extra_span_attributes: dict[str, Any] | None = None,
|
|
171
|
+
) -> GateResult:
|
|
172
|
+
"""Authorize a tool call, record spans, and return a normalized :class:`GateResult`.
|
|
173
|
+
|
|
174
|
+
``can_block`` marks whether the calling integration can actually stop the tool (sets the
|
|
175
|
+
``blocked`` alert attribute accordingly). Records ``tool.invoke`` on allow/observe and
|
|
176
|
+
``tool.block`` on deny/observe — identical span semantics to the TS harness plugins.
|
|
177
|
+
"""
|
|
178
|
+
namespace = resolve_namespace()
|
|
179
|
+
subject_ref = resolve_user_subject(client, subject_fallback)
|
|
180
|
+
label = subject_label(subject_ref)
|
|
181
|
+
check = PermissionCheck(
|
|
182
|
+
namespace=namespace,
|
|
183
|
+
object=tool_name,
|
|
184
|
+
relation="use",
|
|
185
|
+
**apply_subject_to_check_kwargs(subject_ref),
|
|
186
|
+
)
|
|
187
|
+
span_attrs = {"toolName": tool_name, **(extra_span_attributes or {})}
|
|
188
|
+
|
|
189
|
+
# Audit-only kill switch: Ory is disabled entirely — no permission check.
|
|
190
|
+
# Record only the audit ``tool.invoke`` span (same span semantics as the
|
|
191
|
+
# harness plugins' audit-only short-circuit) and pass the tool through.
|
|
192
|
+
resolved = resolve_config()
|
|
193
|
+
if resolved.audit_only:
|
|
194
|
+
client.tracer.record("tool.invoke", "ok", attributes=span_attrs)
|
|
195
|
+
decision = PermissionDecision(
|
|
196
|
+
kind="allow",
|
|
197
|
+
mode=resolved.permission_mode,
|
|
198
|
+
span_attributes={"permissionMode": resolved.permission_mode},
|
|
199
|
+
)
|
|
200
|
+
return GateResult(True, False, decision, label, namespace)
|
|
201
|
+
|
|
202
|
+
decision = gate_tool_call(
|
|
203
|
+
client, harness=harness, tool_name=tool_name, check=check, span_attributes=span_attrs
|
|
204
|
+
)
|
|
205
|
+
decision_attrs = decision.span_attributes
|
|
206
|
+
|
|
207
|
+
if decision.kind == "audit_only":
|
|
208
|
+
# Kill switch: Ory is disabled. Audit the invocation, skip the check.
|
|
209
|
+
client.tracer.record("tool.invoke", "ok", attributes={**span_attrs, "auditOnly": True})
|
|
210
|
+
return GateResult(True, False, decision, label, namespace)
|
|
211
|
+
|
|
212
|
+
if decision.kind == "interactive":
|
|
213
|
+
# user.interaction already recorded by gate_tool_call; pass through.
|
|
214
|
+
return GateResult(True, False, decision, label, namespace)
|
|
215
|
+
|
|
216
|
+
if decision.kind == "fail_open":
|
|
217
|
+
client.logger.warn(
|
|
218
|
+
"permission.fail_open",
|
|
219
|
+
{"tool": tool_name, "code": decision.error.code if decision.error else "unknown"},
|
|
220
|
+
)
|
|
221
|
+
return GateResult(True, False, decision, label, namespace)
|
|
222
|
+
|
|
223
|
+
if decision.kind == "allow":
|
|
224
|
+
client.tracer.record("tool.invoke", "ok", attributes={**span_attrs, **decision_attrs, "allowed": True})
|
|
225
|
+
return GateResult(True, False, decision, label, namespace)
|
|
226
|
+
|
|
227
|
+
if decision.kind == "observe":
|
|
228
|
+
client.tracer.record(
|
|
229
|
+
"tool.block", "denied",
|
|
230
|
+
attributes={**span_attrs, **decision_attrs, "allowed": False, **alert_attributes(False)},
|
|
231
|
+
)
|
|
232
|
+
client.tracer.record(
|
|
233
|
+
"tool.invoke", "ok",
|
|
234
|
+
attributes={**span_attrs, **decision_attrs, "allowed": False, "observed": True},
|
|
235
|
+
)
|
|
236
|
+
return GateResult(True, False, decision, label, namespace)
|
|
237
|
+
|
|
238
|
+
# deny
|
|
239
|
+
client.tracer.record(
|
|
240
|
+
"tool.block", "denied",
|
|
241
|
+
attributes={**span_attrs, **decision_attrs, "allowed": False, **alert_attributes(can_block)},
|
|
242
|
+
)
|
|
243
|
+
message = format_denial_message(DenialContext(tool=tool_name, subject_id=label, namespace=namespace))
|
|
244
|
+
client.logger.warn("tool.denied", {"tool": tool_name, "subjectId": label})
|
|
245
|
+
return GateResult(False, can_block, decision, label, namespace, denial_message=message)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def complete(
|
|
249
|
+
client: OryAgentClient,
|
|
250
|
+
*,
|
|
251
|
+
tool_name: str,
|
|
252
|
+
output: Any = None,
|
|
253
|
+
extra_span_attributes: dict[str, Any] | None = None,
|
|
254
|
+
status: str = "ok",
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Record a ``tool.complete`` span. Never blocks."""
|
|
257
|
+
attrs: dict[str, Any] = {"toolName": tool_name, **(extra_span_attributes or {})}
|
|
258
|
+
if output is not None:
|
|
259
|
+
attrs["hasOutput"] = True
|
|
260
|
+
client.tracer.record("tool.complete", status, attributes=attrs) # type: ignore[arg-type]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def denial_error(result: GateResult, tool_name: str) -> OryDenialError:
|
|
264
|
+
"""Build an :class:`OryDenialError` from a denied :class:`GateResult`."""
|
|
265
|
+
return OryDenialError(
|
|
266
|
+
DenialContext(tool=tool_name, subject_id=result.subject, namespace=result.namespace)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def guarded_tool(
|
|
271
|
+
client: OryAgentClient,
|
|
272
|
+
*,
|
|
273
|
+
harness: str,
|
|
274
|
+
tool_name: str,
|
|
275
|
+
execute: Callable[[Any], Any],
|
|
276
|
+
can_block: bool = True,
|
|
277
|
+
):
|
|
278
|
+
"""Wrap a plain ``execute(args)`` callable with gate + complete.
|
|
279
|
+
|
|
280
|
+
On allow/observe/fail-open/interactive the wrapped tool runs and a ``tool.complete``
|
|
281
|
+
span is recorded. On a hard deny it raises :class:`OryDenialError` (the veto mechanism
|
|
282
|
+
for tool-boundary SDKs). Returns a callable with the same ``(args)`` signature.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def wrapped(args: Any = None):
|
|
286
|
+
result = gate(client, harness=harness, tool_name=tool_name, tool_args=args, can_block=can_block)
|
|
287
|
+
if result.blocked:
|
|
288
|
+
raise OryDenialError(
|
|
289
|
+
DenialContext(tool=tool_name, subject_id=result.subject, namespace=result.namespace)
|
|
290
|
+
)
|
|
291
|
+
output = execute(args)
|
|
292
|
+
complete(client, tool_name=tool_name, output=output)
|
|
293
|
+
return output
|
|
294
|
+
|
|
295
|
+
return wrapped
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__ = [
|
|
299
|
+
"SessionStartResult",
|
|
300
|
+
"GateResult",
|
|
301
|
+
"resolve_namespace",
|
|
302
|
+
"session_start",
|
|
303
|
+
"register_subagent",
|
|
304
|
+
"gate",
|
|
305
|
+
"complete",
|
|
306
|
+
"guarded_tool",
|
|
307
|
+
"denial_error",
|
|
308
|
+
]
|