gargoyle-receipts 0.5.0.dev0__tar.gz → 0.5.2.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.2.dev0}/PKG-INFO +2 -2
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/README.md +1 -1
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/entra_login.py +45 -4
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0/gargoyle_receipts.egg-info}/PKG-INFO +2 -2
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_entra_login.py +43 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/evidence.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/issuer_service.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/mcp_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/published_record_source.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/static/approve.html +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/static/approve.js +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_cli.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_console_js.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_issuer_service.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_mcp_self_issue.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_rules.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.dev0}/tests/test_wire_spec.py +0 -0
{gargoyle_receipts-0.5.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.5.2.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.2.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.2.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.2.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,30 @@ 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. requests applies the environment
|
|
77
|
+
bundle per request and lets it beat Session.verify, so the override is
|
|
78
|
+
made per request too. Proxies from the environment still apply."""
|
|
79
|
+
import certifi
|
|
80
|
+
import requests
|
|
81
|
+
|
|
82
|
+
class PublicRootsSession(requests.Session):
|
|
83
|
+
def request(self, method, url, **kwargs):
|
|
84
|
+
if kwargs.get("verify") in (None, True):
|
|
85
|
+
kwargs["verify"] = certifi.where()
|
|
86
|
+
return super().request(method, url, **kwargs)
|
|
87
|
+
|
|
88
|
+
return PublicRootsSession()
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
def _app(tenant: str, client_id: str, cache: TokenCache, app_factory: Optional[Callable] = None) -> Any:
|
|
73
92
|
if app_factory is not None:
|
|
74
93
|
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
|
|
94
|
+
return _msal().PublicClientApplication(client_id, authority=AUTHORITY.format(tenant=tenant), token_cache=cache.cache,
|
|
95
|
+
http_client=public_roots_http())
|
|
76
96
|
|
|
77
97
|
|
|
78
98
|
def device_code_login(tenant: str, client_id: str, scope: str, cache_path: str, *,
|
|
@@ -105,8 +125,26 @@ class EntraTokenSource:
|
|
|
105
125
|
def __init__(self, tenant: str, client_id: str, scope: str, cache_path: str, *,
|
|
106
126
|
app_factory: Optional[Callable] = None, msal_module: Any = None) -> None:
|
|
107
127
|
self.scope = scope
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
128
|
+
self._args = (tenant, client_id, cache_path, app_factory, msal_module)
|
|
129
|
+
self._cache: Optional[TokenCache] = None
|
|
130
|
+
self._app_obj: Any = None
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def _app(self) -> Any:
|
|
134
|
+
# Built on first use: MSAL fetches the tenant's OpenID configuration
|
|
135
|
+
# when the application object is created, and that must not happen
|
|
136
|
+
# while the MCP client is starting the middleware, nor kill it if
|
|
137
|
+
# the network is down. A failure here surfaces as LoginRequired on
|
|
138
|
+
# the call, which the middleware turns into a refusal.
|
|
139
|
+
if self._app_obj is None:
|
|
140
|
+
tenant, client_id, cache_path, app_factory, msal_module = self._args
|
|
141
|
+
self._cache = TokenCache(cache_path, msal_module)
|
|
142
|
+
try:
|
|
143
|
+
self._app_obj = _app(tenant, client_id, self._cache, app_factory)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
raise LoginRequired("Entra could not be reached to validate the sign-in: %s: %s"
|
|
146
|
+
% (type(exc).__name__, str(exc)[:200])) from exc
|
|
147
|
+
return self._app_obj
|
|
110
148
|
|
|
111
149
|
def token(self) -> str:
|
|
112
150
|
accounts: List[Dict[str, Any]] = self._app.get_accounts()
|
|
@@ -121,7 +159,10 @@ class EntraTokenSource:
|
|
|
121
159
|
|
|
122
160
|
@property
|
|
123
161
|
def principal(self) -> Optional[str]:
|
|
124
|
-
|
|
162
|
+
try:
|
|
163
|
+
accounts = self._app.get_accounts()
|
|
164
|
+
except LoginRequired:
|
|
165
|
+
return None
|
|
125
166
|
return accounts[0].get("username") if accounts else None
|
|
126
167
|
|
|
127
168
|
|
{gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.2.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.2.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.2.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.2.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,46 @@ 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 # one MSAL app, with our session
|
|
152
|
+
# Through requests' own environment merge: the private CA in REQUESTS_CA_BUNDLE loses.
|
|
153
|
+
import requests
|
|
154
|
+
sent = {}
|
|
155
|
+
def fake_send(self, prep, **kw):
|
|
156
|
+
sent.update(kw); r = requests.Response(); r.status_code = 200; r._content = b"{}"; return r
|
|
157
|
+
monkeypatch.setattr(requests.Session, "send", fake_send)
|
|
158
|
+
built[0].get("https://login.microsoftonline.com/x/v2.0/.well-known/openid-configuration")
|
|
159
|
+
assert sent["verify"] == certifi.where()
|
|
160
|
+
|
|
161
|
+
# A tenant that cannot be reached is a refusal at call time, not a crash at startup.
|
|
162
|
+
class Unreachable(FakeMsal):
|
|
163
|
+
class PublicClientApplication:
|
|
164
|
+
def __init__(self, *a, **k): raise OSError("name resolution failed")
|
|
165
|
+
monkeypatch.setattr(entra_login, "_msal", lambda: Unreachable)
|
|
166
|
+
off = EntraTokenSource("t", "c", "s", str(cache), msal_module=Unreachable)
|
|
167
|
+
with pytest.raises(LoginRequired, match="could not be reached"):
|
|
168
|
+
off.token()
|
|
169
|
+
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.2.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.2.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.2.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.2.dev0}/tests/test_typescript_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|