gargoyle-receipts 0.5.0.dev0__tar.gz → 0.5.1.dev0__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.
- {gargoyle_receipts-0.5.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.5.1.dev0}/PKG-INFO +2 -2
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/README.md +1 -1
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/entra_login.py +39 -4
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0/gargoyle_receipts.egg-info}/PKG-INFO +2 -2
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_entra_login.py +35 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/evidence.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/issuer_service.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/mcp_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/published_record_source.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/static/approve.html +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/static/approve.js +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_console_js.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_issuer_service.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_self_issue.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_rules.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_wire_spec.py +0 -0
{gargoyle_receipts-0.5.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.5.1.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1.dev0
|
|
4
4
|
Summary: Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls.
|
|
5
5
|
Author: Justin LaPorte
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -59,7 +59,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
59
59
|
|
|
60
60
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.5.
|
|
62
|
+
Status: pre-release (`0.5.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
63
63
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
64
64
|
|
|
65
65
|
## What a receipt gives you
|
|
@@ -21,7 +21,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
21
21
|
|
|
22
22
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
23
23
|
|
|
24
|
-
Status: pre-release (`0.5.
|
|
24
|
+
Status: pre-release (`0.5.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
25
25
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
26
26
|
|
|
27
27
|
## What a receipt gives you
|
|
@@ -69,10 +69,24 @@ class TokenCache:
|
|
|
69
69
|
pass
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def public_roots_http():
|
|
73
|
+
"""A requests session that trusts the public CA roots (certifi) whatever
|
|
74
|
+
REQUESTS_CA_BUNDLE says. A deployment pins the issuer's private CA with
|
|
75
|
+
that variable; Entra's endpoints are not signed by it, so the two trust
|
|
76
|
+
domains must not share one setting. Proxies from the environment still
|
|
77
|
+
apply."""
|
|
78
|
+
import certifi
|
|
79
|
+
import requests
|
|
80
|
+
s = requests.Session()
|
|
81
|
+
s.verify = certifi.where()
|
|
82
|
+
return s
|
|
83
|
+
|
|
84
|
+
|
|
72
85
|
def _app(tenant: str, client_id: str, cache: TokenCache, app_factory: Optional[Callable] = None) -> Any:
|
|
73
86
|
if app_factory is not None:
|
|
74
87
|
return app_factory(client_id=client_id, authority=AUTHORITY.format(tenant=tenant), token_cache=cache.cache)
|
|
75
|
-
return _msal().PublicClientApplication(client_id, authority=AUTHORITY.format(tenant=tenant), token_cache=cache.cache
|
|
88
|
+
return _msal().PublicClientApplication(client_id, authority=AUTHORITY.format(tenant=tenant), token_cache=cache.cache,
|
|
89
|
+
http_client=public_roots_http())
|
|
76
90
|
|
|
77
91
|
|
|
78
92
|
def device_code_login(tenant: str, client_id: str, scope: str, cache_path: str, *,
|
|
@@ -105,8 +119,26 @@ class EntraTokenSource:
|
|
|
105
119
|
def __init__(self, tenant: str, client_id: str, scope: str, cache_path: str, *,
|
|
106
120
|
app_factory: Optional[Callable] = None, msal_module: Any = None) -> None:
|
|
107
121
|
self.scope = scope
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
122
|
+
self._args = (tenant, client_id, cache_path, app_factory, msal_module)
|
|
123
|
+
self._cache: Optional[TokenCache] = None
|
|
124
|
+
self._app_obj: Any = None
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def _app(self) -> Any:
|
|
128
|
+
# Built on first use: MSAL fetches the tenant's OpenID configuration
|
|
129
|
+
# when the application object is created, and that must not happen
|
|
130
|
+
# while the MCP client is starting the middleware, nor kill it if
|
|
131
|
+
# the network is down. A failure here surfaces as LoginRequired on
|
|
132
|
+
# the call, which the middleware turns into a refusal.
|
|
133
|
+
if self._app_obj is None:
|
|
134
|
+
tenant, client_id, cache_path, app_factory, msal_module = self._args
|
|
135
|
+
self._cache = TokenCache(cache_path, msal_module)
|
|
136
|
+
try:
|
|
137
|
+
self._app_obj = _app(tenant, client_id, self._cache, app_factory)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
raise LoginRequired("Entra could not be reached to validate the sign-in: %s: %s"
|
|
140
|
+
% (type(exc).__name__, str(exc)[:200])) from exc
|
|
141
|
+
return self._app_obj
|
|
110
142
|
|
|
111
143
|
def token(self) -> str:
|
|
112
144
|
accounts: List[Dict[str, Any]] = self._app.get_accounts()
|
|
@@ -121,7 +153,10 @@ class EntraTokenSource:
|
|
|
121
153
|
|
|
122
154
|
@property
|
|
123
155
|
def principal(self) -> Optional[str]:
|
|
124
|
-
|
|
156
|
+
try:
|
|
157
|
+
accounts = self._app.get_accounts()
|
|
158
|
+
except LoginRequired:
|
|
159
|
+
return None
|
|
125
160
|
return accounts[0].get("username") if accounts else None
|
|
126
161
|
|
|
127
162
|
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0/gargoyle_receipts.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1.dev0
|
|
4
4
|
Summary: Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls.
|
|
5
5
|
Author: Justin LaPorte
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -59,7 +59,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
59
59
|
|
|
60
60
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.5.
|
|
62
|
+
Status: pre-release (`0.5.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
63
63
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
64
64
|
|
|
65
65
|
## What a receipt gives you
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gargoyle-receipts"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1.dev0"
|
|
8
8
|
description = "Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -124,3 +124,38 @@ def test_login_command_rewrites_the_client_config(tmp_path, monkeypatch, capsys)
|
|
|
124
124
|
# Everything the middleware needs to pick the Entra source is there.
|
|
125
125
|
for k in ("GARGOYLE_MCP_ENTRA_TENANT", "GARGOYLE_MCP_ENTRA_CLIENT_ID", "GARGOYLE_MCP_ENTRA_SCOPE", "GARGOYLE_MCP_ENTRA_TOKEN_CACHE"):
|
|
126
126
|
assert env[k] and mcp[k] == env[k]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_msal_gets_a_public_roots_http_client_and_is_built_lazily(tmp_path, monkeypatch):
|
|
130
|
+
"""REQUESTS_CA_BUNDLE pins the issuer's private CA for the middleware; MSAL
|
|
131
|
+
must still trust the public roots. And nothing touches the network until
|
|
132
|
+
the first token is asked for."""
|
|
133
|
+
from gargoyle import entra_login
|
|
134
|
+
import certifi
|
|
135
|
+
built = []
|
|
136
|
+
|
|
137
|
+
class CapturingMsal(FakeMsal):
|
|
138
|
+
class PublicClientApplication:
|
|
139
|
+
def __init__(self, client_id, authority, token_cache, http_client=None):
|
|
140
|
+
built.append(http_client)
|
|
141
|
+
self.inner = FakeApp(client_id, authority, token_cache)
|
|
142
|
+
def get_accounts(self): return self.inner.get_accounts()
|
|
143
|
+
def acquire_token_silent(self, scopes, account): return self.inner.acquire_token_silent(scopes, account)
|
|
144
|
+
monkeypatch.setattr(entra_login, "_msal", lambda: CapturingMsal)
|
|
145
|
+
monkeypatch.setenv("REQUESTS_CA_BUNDLE", str(tmp_path / "private-ca.crt"))
|
|
146
|
+
cache = tmp_path / "cache.json"
|
|
147
|
+
device_code_login("t", "c", "s", str(cache), app_factory=FakeApp, msal_module=FakeMsal, prompt=lambda m: None)
|
|
148
|
+
src = EntraTokenSource("t", "c", "s", str(cache), msal_module=CapturingMsal)
|
|
149
|
+
assert built == [] # constructed nothing yet
|
|
150
|
+
assert src.token() == "at-refreshed"
|
|
151
|
+
assert len(built) == 1 and built[0].verify == certifi.where() # public roots, not the private CA
|
|
152
|
+
|
|
153
|
+
# A tenant that cannot be reached is a refusal at call time, not a crash at startup.
|
|
154
|
+
class Unreachable(FakeMsal):
|
|
155
|
+
class PublicClientApplication:
|
|
156
|
+
def __init__(self, *a, **k): raise OSError("name resolution failed")
|
|
157
|
+
monkeypatch.setattr(entra_login, "_msal", lambda: Unreachable)
|
|
158
|
+
off = EntraTokenSource("t", "c", "s", str(cache), msal_module=Unreachable)
|
|
159
|
+
with pytest.raises(LoginRequired, match="could not be reached"):
|
|
160
|
+
off.token()
|
|
161
|
+
assert off.principal is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/published_record_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_published_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_typescript_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|