pushary 2.1.2__tar.gz → 2.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pushary
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: The decision layer for AI agents. Your agent asks, your user decides on their phone.
5
5
  Project-URL: Homepage, https://pushary.com
6
6
  Project-URL: Documentation, https://pushary.com/docs
@@ -287,3 +287,7 @@ Shared approval gates now bind decision identity to the customer, session, tool
287
287
  These keys intentionally differ from 2.0. Do not replay completed operations during upgrade or silently migrate an old approval onto a new key. Drain pending runs on their original version, or explicitly request fresh review while preserving your application's operation idempotency. `policy=False` now records the full subject and presentation on the human decision; a failed legacy policy endpoint retains the prior minimal fallback for compatibility.
288
288
 
289
289
  `decisions.create` and `decisions.ask` accept `presentation`, using the existing server-validated shape with `label`, `effect`, optional `risk`, and `changes` that reference keys in `parameters`. Shared `ask_human` and `create_durable_decision` helpers forward that field plus target, actor, environment, parameters, placeholder, expiry, reachability, and callback settings. Parameter values remain the source of truth for display; presentation changes name a parameter rather than carrying a second copy of its value.
290
+
291
+ ### Upgrading to 2.2
292
+
293
+ `ApprovalAsk` accepts `context`. The shared approval gate sends it with the decision, so the approver can read what the notification cannot fit, such as a tool's full arguments. Context is part of the approval identity when set. An ask without context keeps the key it had in 2.1, so pending approvals carry over.
@@ -259,3 +259,7 @@ Shared approval gates now bind decision identity to the customer, session, tool
259
259
  These keys intentionally differ from 2.0. Do not replay completed operations during upgrade or silently migrate an old approval onto a new key. Drain pending runs on their original version, or explicitly request fresh review while preserving your application's operation idempotency. `policy=False` now records the full subject and presentation on the human decision; a failed legacy policy endpoint retains the prior minimal fallback for compatibility.
260
260
 
261
261
  `decisions.create` and `decisions.ask` accept `presentation`, using the existing server-validated shape with `label`, `effect`, optional `risk`, and `changes` that reference keys in `parameters`. Shared `ask_human` and `create_durable_decision` helpers forward that field plus target, actor, environment, parameters, placeholder, expiry, reachability, and callback settings. Parameter values remain the source of truth for display; presentation changes name a parameter rather than carrying a second copy of its value.
262
+
263
+ ### Upgrading to 2.2
264
+
265
+ `ApprovalAsk` accepts `context`. The shared approval gate sends it with the decision, so the approver can read what the notification cannot fit, such as a tool's full arguments. Context is part of the approval identity when set. An ask without context keeps the key it had in 2.1, so pending approvals carry over.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pushary"
7
- version = "2.1.2"
7
+ version = "2.2.0"
8
8
  description = "The decision layer for AI agents. Your agent asks, your user decides on their phone."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -22,7 +22,7 @@ from .keys import KeysResource
22
22
  from .errors import PusharyError
23
23
  from .webhook import SIGNATURE_HEADER, parse_decision_callback, verify_webhook_signature
24
24
 
25
- __version__ = "2.1.2"
25
+ __version__ = "2.2.0"
26
26
 
27
27
  __all__ = [
28
28
  "PusharyServer",
@@ -153,6 +153,7 @@ def _gate_key(ask: "ApprovalAsk", facts: Optional[Dict[str, Any]]) -> str:
153
153
  "input": ask.input,
154
154
  "parameters": facts,
155
155
  "presentation": ask.presentation,
156
+ **({"context": ask.context} if ask.context is not None else {}),
156
157
  })
157
158
 
158
159
 
@@ -251,6 +252,7 @@ class ApprovalAsk:
251
252
  "input",
252
253
  "parameters",
253
254
  "presentation",
255
+ "context",
254
256
  )
255
257
 
256
258
  def __init__(
@@ -267,6 +269,7 @@ class ApprovalAsk:
267
269
  input: Any = None,
268
270
  parameters: Optional[Dict[str, Any]] = None,
269
271
  presentation: Optional[Dict[str, Any]] = None,
272
+ context: Optional[str] = None,
270
273
  ) -> None:
271
274
  self.tool_name = tool_name
272
275
  self.call_id = call_id
@@ -286,6 +289,7 @@ class ApprovalAsk:
286
289
  #: Explicit facts for policy, used instead of deriving them from ``input``.
287
290
  self.parameters = parameters
288
291
  self.presentation = presentation
292
+ self.context = context
289
293
 
290
294
 
291
295
  class ApprovalDecision:
@@ -689,6 +693,7 @@ class AdapterKernel:
689
693
  ask.question,
690
694
  type="confirm",
691
695
  external_id=ask.external_id,
696
+ context=ask.context,
692
697
  agent_name=agent_name,
693
698
  expires_in_seconds=expires_in_seconds,
694
699
  timeout_seconds=timeout_seconds,
@@ -268,6 +268,55 @@ class GateTests(unittest.TestCase):
268
268
  self.assertEqual(keys[0], keys[1])
269
269
  self.assertNotEqual(keys[0], keys[2])
270
270
 
271
+ def test_shows_the_approver_the_context(self):
272
+ decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
273
+ with WithFakeClient(FakeClient(decisions=decisions)):
274
+ kernel.create_gate()(
275
+ ApprovalAsk(
276
+ tool_name="issue_refund",
277
+ call_id="call_1",
278
+ session_id="sess_1",
279
+ question="Approve?",
280
+ external_id="user_1",
281
+ context='Tool arguments: {"amount": 99999}',
282
+ )
283
+ )
284
+ self.assertEqual(decisions.ask_calls[0]["context"], 'Tool arguments: {"amount": 99999}')
285
+
286
+ def test_context_is_part_of_the_approval_identity(self):
287
+ decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
288
+ with WithFakeClient(FakeClient(decisions=decisions)):
289
+ gate = kernel.create_gate()
290
+ for context in ("Refund 5", "Refund 99999"):
291
+ gate(
292
+ ApprovalAsk(
293
+ tool_name="issue_refund",
294
+ call_id="call_1",
295
+ session_id="sess_1",
296
+ question="Approve?",
297
+ external_id="user_1",
298
+ context=context,
299
+ )
300
+ )
301
+ first, second = (call["idempotency_key"] for call in decisions.ask_calls)
302
+ self.assertNotEqual(first, second)
303
+
304
+ def test_an_ask_without_context_keeps_the_key_it_had_before_context_existed(self):
305
+ decisions = FakeDecisions(ask_result={"answered": True, "approved": True, "value": "yes", "decisionId": "d1"})
306
+ with WithFakeClient(FakeClient(decisions=decisions)):
307
+ kernel.create_gate()(
308
+ ApprovalAsk(
309
+ tool_name="issue_refund",
310
+ call_id="call_1",
311
+ session_id="sess_1",
312
+ question="Approve?",
313
+ external_id="user_1",
314
+ input={"amount": 5},
315
+ )
316
+ )
317
+ self.assertIsNone(decisions.ask_calls[0]["context"])
318
+ self.assertEqual(decisions.ask_calls[0]["idempotency_key"], "135e90787543c78d72b7008ce463da08d1ce8769")
319
+
271
320
  def test_allows_without_opening_a_decision_or_paging_anyone(self):
272
321
  decisions = FakeDecisions()
273
322
  client = FakeClient(
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