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.
Files changed (33) hide show
  1. ory_argus-0.13.9/.gitignore +36 -0
  2. ory_argus-0.13.9/PKG-INFO +42 -0
  3. ory_argus-0.13.9/README.md +23 -0
  4. ory_argus-0.13.9/pyproject.toml +29 -0
  5. ory_argus-0.13.9/src/ory_argus/__init__.py +238 -0
  6. ory_argus-0.13.9/src/ory_argus/adapters.py +308 -0
  7. ory_argus-0.13.9/src/ory_argus/agent_auth.py +576 -0
  8. ory_argus-0.13.9/src/ory_argus/auth.py +328 -0
  9. ory_argus-0.13.9/src/ory_argus/auth_store.py +171 -0
  10. ory_argus-0.13.9/src/ory_argus/cli.py +608 -0
  11. ory_argus-0.13.9/src/ory_argus/client.py +604 -0
  12. ory_argus-0.13.9/src/ory_argus/config.py +571 -0
  13. ory_argus-0.13.9/src/ory_argus/denial.py +108 -0
  14. ory_argus-0.13.9/src/ory_argus/lifecycle.py +216 -0
  15. ory_argus-0.13.9/src/ory_argus/logger.py +108 -0
  16. ory_argus-0.13.9/src/ory_argus/mcp.py +14 -0
  17. ory_argus-0.13.9/src/ory_argus/otel.py +368 -0
  18. ory_argus-0.13.9/src/ory_argus/permissions.py +231 -0
  19. ory_argus-0.13.9/src/ory_argus/subject.py +79 -0
  20. ory_argus-0.13.9/src/ory_argus/testing.py +188 -0
  21. ory_argus-0.13.9/src/ory_argus/tool_catalog.py +107 -0
  22. ory_argus-0.13.9/src/ory_argus/tracer.py +498 -0
  23. ory_argus-0.13.9/src/ory_argus/types.py +130 -0
  24. ory_argus-0.13.9/src/ory_argus/user_login.py +266 -0
  25. ory_argus-0.13.9/tests/test_adapters.py +265 -0
  26. ory_argus-0.13.9/tests/test_auth.py +1114 -0
  27. ory_argus-0.13.9/tests/test_cli.py +434 -0
  28. ory_argus-0.13.9/tests/test_client.py +452 -0
  29. ory_argus-0.13.9/tests/test_config.py +268 -0
  30. ory_argus-0.13.9/tests/test_e2e.py +85 -0
  31. ory_argus-0.13.9/tests/test_lifecycle.py +71 -0
  32. ory_argus-0.13.9/tests/test_permissions.py +250 -0
  33. 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
+ ]