pushary 2.2.0__tar.gz → 2.2.1__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.2.0
3
+ Version: 2.2.1
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
@@ -83,14 +83,20 @@ export DIFY_NODE_ID='review'
83
83
  second workflow. A pending result is reported as `blocked` with the SDK's reason.
84
84
  After approval, the result contains the released local order. Inspect `orders.db`
85
85
  inside the state directory: the business idempotency key is unique. Re-running
86
- `resume` after submission stops because the native form is already closed.
86
+ `resume` after a committed order returns that order with `"recovered": true`, using
87
+ read-only backend evidence even when the native form is closed or approval has expired.
88
+ This reports an existing effect; it does not authorize another one.
87
89
 
88
90
  For your backend, import `start` and `resume` from [review.py](review.py). Supply
89
91
  tenant/customer IDs and the action from authenticated application state, never
90
92
  model output. The CLI's `demo-store` tenant and test order are demonstration data.
91
93
  Pass your business callback to `resume`; it receives a copy of the exact approved
92
94
  parameters and a stable idempotency key. Your downstream service must also enforce
93
- that key if it can retry internally.
95
+ that key if it can retry internally. Optionally supply a trusted, read-only `lookup`
96
+ callback with the same arguments: return the verified committed result, return `None`
97
+ when completion is unconfirmed, or raise when evidence is unavailable or contradictory.
98
+ Without `lookup`, the original stop-and-reconcile behavior is unchanged. Never use a
99
+ Dify success flag, approval answer, or worker-local success cache as backend evidence.
94
100
 
95
101
  ## Trust and recovery limits
96
102
 
@@ -107,14 +113,57 @@ that key if it can retry internally.
107
113
  the one-hour Pushary request; both deadlines are checked before the effect.
108
114
  - The SDK consumes the remote one-use permit **before** native submission. Dify
109
115
  also makes forms one-shot. A lost submission response, native resume taking over
110
- 30 seconds, worker crash or uncertain business result needs operator reconciliation.
111
- The spent permit is not reset and the effect is not automatically retried.
116
+ 30 seconds, worker crash or uncertain business result needs reconciliation.
117
+ The SQLite demo automatically reads back a committed order and its matching backend
118
+ receipt. Without that evidence it stops for operator reconciliation. The spent
119
+ permit is not reset and the effect is not automatically retried.
112
120
  - If initial workflow creation fails before the pause is saved, inspect Dify's run
113
121
  logs. Do not delete the state directory and blindly start another operation.
114
122
  - Dify workspace administrators and anyone who controls your backend remain trusted.
115
123
  This example does not intercept arbitrary agent tools or secure a separate effect
116
124
  placed directly in Dify.
117
125
 
126
+ ## Recovery evidence and interruption checkpoints
127
+
128
+ The demo's backend is `orders.db`, separate from Dify and the Pushary permit service.
129
+ `release_order` commits the order and a receipt containing **all** approved parameters
130
+ under the same binding in one SQLite transaction. `lookup_order` opens that database
131
+ read-only and reads both rows in one snapshot. Both must match before it reports
132
+ `released`; a missing row, mismatched parameters, corrupt database or legacy schema
133
+ cannot become recovered success. Existing databases without the new receipt table
134
+ require manual reconciliation; do not delete them to force a fresh execution.
135
+
136
+ The binding includes tenant, customer, native run, workflow version, form, parameters
137
+ and deadline. Caller identity and saved operation integrity are checked before lookup.
138
+ Only the permit-protected callback writes orders. Lookup never submits a form, consumes
139
+ a permit, executes an effect or changes a receipt. A late callback exception triggers
140
+ another readback before the SDK reports an outcome. Verified completion therefore
141
+ survives that exception. There is no mutable local completion status for an older
142
+ worker to overwrite; the committed order and receipt remain the source of truth.
143
+
144
+ | Interruption | Backend evidence | Restart outcome | New effect |
145
+ | --- | --- | --- | --- |
146
+ | Permit consumed, response lost | No order | Blocked/unconfirmed permit | None |
147
+ | Native form committed, response lost | No order | Stop for reconciliation | None |
148
+ | Worker dies before backend effect | No order | Stop for reconciliation | None |
149
+ | Order and receipt commit, worker dies before returning | Matching order and receipt | `released`, `recovered: true` | None |
150
+ | Worker B verifies completion before worker A's delayed exception | Matching order and receipt | B recovers; A reads back success; subsequent resumes recover | None |
151
+ | SDK receipt reporting is unavailable | Matching order and receipt | Backend success remains recoverable | None |
152
+ | Backend evidence is missing, inconsistent or unreadable | Unverified | Stop; never invent success | None |
153
+
154
+ The backend receipt is distinct from Pushary's hosted execution receipt. A process
155
+ killed before SDK reporting can leave the hosted permit marked running; this example
156
+ **does not repair that hosted receipt** or claim that its status proves the business
157
+ outcome. After a recoverable delayed callback exception, the still-running worker can
158
+ report SDK success normally. An operator must reconcile a receipt left by a dead worker.
159
+
160
+ This is a local synthetic comparison with [Mission's proposed checkpoint](https://github.com/langgenius/dify/discussions/42319#discussioncomment-18625206): backend
161
+ commit → lost worker response → fresh-process lookup. It is not a Mission integration,
162
+ a distributed exactly-once guarantee, or proof of live Dify/phone-service recovery.
163
+ For a remote backend, implement lookup using that provider's authoritative operation
164
+ identifier, exact subject and durable committed evidence; an absent lookup result alone
165
+ must never authorize resending an uncertain operation.
166
+
118
167
  ## Verification
119
168
 
120
169
  ```bash
@@ -126,9 +175,14 @@ The checks use Dify's **real Graphon 0.7.0** Human Input node, graph engine and
126
175
  serialized runtime snapshots, plus the published Pushary SDK. They run fresh
127
176
  processes across pause/resume and cover pending, approval, denial, expiry,
128
177
  cancellation, customer/version/parameter changes, duplicate workers, a competing
129
- native submitter, lost permit/submission responses and a crash after the effect.
130
-
131
- Both hosted HTTP services and the order service are simulated in these checks.
178
+ native submitter, lost permit/submission responses and crashes before/after the effect.
179
+ They use the actual SQLite order writer and readback, compare restart with and without
180
+ lookup, assert one order and one matching receipt, and exercise worker B's recovery
181
+ before worker A's delayed error. They also reject corrupt, incomplete, legacy and
182
+ mismatched evidence and verify readback after approval expiry.
183
+
184
+ Both hosted HTTP services are simulated; the test backend uses real local SQLite
185
+ transactions and the demo's order writer/readback.
132
186
  Dify Cloud import and a Studio pause → approve → resume smoke test were also
133
187
  verified on 2026-09-15. The automated checks do **not** prove its database/Celery
134
188
  delivery or live phone delivery. Run the test-phone procedure above against your deployment
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pushary"
7
- version = "2.2.0"
7
+ version = "2.2.1"
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.2.0"
25
+ __version__ = "2.2.1"
26
26
 
27
27
  __all__ = [
28
28
  "PusharyServer",
@@ -24,14 +24,14 @@ DEFAULT_BASE_URL = "https://pushary.com/api/v1/server"
24
24
  # little above it to leave room for the round trip without hanging forever.
25
25
  DEFAULT_TIMEOUT_SECONDS = 65.0
26
26
 
27
- _SETTINGS_URL = "https://pushary.com/dashboard/settings"
27
+ _SETTINGS_URL = "https://pushary.com/dashboard/agent/settings"
28
28
 
29
29
 
30
30
  class PusharyServer:
31
31
  """Client for the Pushary human-in-the-loop decisions API.
32
32
 
33
33
  Args:
34
- api_key: The full API key in the form ``pk_xxx.sk_xxx``.
34
+ api_key: The full API key in the form ``pk_xxx.xxx``.
35
35
  base_url: Override the API base URL. Defaults to the public endpoint.
36
36
 
37
37
  Raises:
@@ -244,7 +244,7 @@ class PusharyServer:
244
244
  )
245
245
  if "." not in api_key:
246
246
  raise ValueError(
247
- "Invalid API key format. Use the full API key (pk_xxx.sk_xxx). "
247
+ "Invalid API key format. Use the full API key (pk_xxx.xxx). "
248
248
  f"Get your API key from {_SETTINGS_URL}"
249
249
  )
250
250
 
@@ -3,8 +3,10 @@
3
3
  For multi-tenant Partners. Call ``issue`` from your backend with your own
4
4
  (unbound, Partner-plan) key to mint a child key bound to a single end-user, hand
5
5
  that key to the agent runtime acting for that user, then ``revoke`` it when the
6
- session ends. A bound key can only create/resolve decisions and enroll that exact
7
- end-user, so a prompt-injected agent can never reach another of your users.
6
+ session ends. A bound key can only create, read and cancel decisions for that
7
+ exact end-user, so a prompt-injected agent can never reach another of your users.
8
+ It cannot enroll devices or answer a decision; use your server-side full-access
9
+ key for those operations.
8
10
  """
9
11
 
10
12
  from __future__ import annotations
@@ -38,12 +38,12 @@ class ApiKeyValidationTests(unittest.TestCase):
38
38
  def test_empty_key_raises_value_error(self):
39
39
  with self.assertRaises(ValueError) as ctx:
40
40
  PusharyServer(api_key="")
41
- self.assertIn("dashboard/settings", str(ctx.exception))
41
+ self.assertIn("dashboard/agent/settings", str(ctx.exception))
42
42
 
43
43
  def test_key_without_dot_raises_value_error(self):
44
44
  with self.assertRaises(ValueError) as ctx:
45
45
  PusharyServer(api_key="pk_live_only")
46
- self.assertIn("pk_xxx.sk_xxx", str(ctx.exception))
46
+ self.assertIn("pk_xxx.xxx", str(ctx.exception))
47
47
 
48
48
  def test_valid_key_constructs(self):
49
49
  client = PusharyServer(api_key=VALID_KEY)
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