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.
Files changed (65) hide show
  1. {gargoyle_receipts-0.5.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.5.1.dev0}/PKG-INFO +2 -2
  2. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/README.md +1 -1
  3. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/entra_login.py +39 -4
  4. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0/gargoyle_receipts.egg-info}/PKG-INFO +2 -2
  5. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/pyproject.toml +1 -1
  6. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_entra_login.py +35 -0
  7. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/LICENSE +0 -0
  8. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/NOTICE +0 -0
  9. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/__init__.py +0 -0
  10. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approval.py +0 -0
  11. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approver_cli.py +0 -0
  12. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/approver_trust.py +0 -0
  13. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/cli.py +0 -0
  14. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/consumer.py +0 -0
  15. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/envelope.py +0 -0
  16. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/evidence.py +0 -0
  17. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/execution.py +0 -0
  18. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/fs_server.py +0 -0
  19. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/identity.py +0 -0
  20. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/issuer.py +0 -0
  21. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/issuer_service.py +0 -0
  22. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/jws.py +0 -0
  23. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/key_record_source.py +0 -0
  24. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/mcp_server.py +0 -0
  25. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/notify.py +0 -0
  26. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/opa.py +0 -0
  27. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/pending_store.py +0 -0
  28. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/policy.py +0 -0
  29. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/published_record_source.py +0 -0
  30. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/replay_cache.py +0 -0
  31. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/rules.py +0 -0
  32. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/static/approve.html +0 -0
  33. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/static/approve.js +0 -0
  34. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle/verifier.py +0 -0
  35. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
  36. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
  37. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
  38. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
  39. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
  40. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/setup.cfg +0 -0
  41. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_annotations.py +0 -0
  42. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_approver_cli.py +0 -0
  43. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_canonical.py +0 -0
  44. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_cli.py +0 -0
  45. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_console_js.py +0 -0
  46. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_consumer.py +0 -0
  47. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_entra_identity.py +0 -0
  48. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_evidence.py +0 -0
  49. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_execution.py +0 -0
  50. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_fs_server.py +0 -0
  51. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_identity.py +0 -0
  52. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_issuer_service.py +0 -0
  53. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_jws.py +0 -0
  54. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_proxy.py +0 -0
  55. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_self_issue.py +0 -0
  56. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_mcp_server.py +0 -0
  57. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_notify.py +0 -0
  58. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_opa.py +0 -0
  59. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_policy.py +0 -0
  60. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_published_record.py +0 -0
  61. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_rules.py +0 -0
  62. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_stdio_encoding.py +0 -0
  63. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_threshold.py +0 -0
  64. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_typescript_conformance.py +0 -0
  65. {gargoyle_receipts-0.5.0.dev0 → gargoyle_receipts-0.5.1.dev0}/tests/test_wire_spec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gargoyle-receipts
3
- Version: 0.5.0.dev0
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
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
61
61
 
62
- Status: pre-release (`0.5.0.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
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
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
23
23
 
24
- Status: pre-release (`0.5.0.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
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._cache = TokenCache(cache_path, msal_module)
109
- self._app = _app(tenant, client_id, self._cache, app_factory)
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
- accounts = self._app.get_accounts()
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gargoyle-receipts
3
- Version: 0.5.0.dev0
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
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
61
61
 
62
- Status: pre-release (`0.5.0.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
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.0.dev0"
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