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.
- {pushary-2.2.0 → pushary-2.2.1}/PKG-INFO +1 -1
- {pushary-2.2.0 → pushary-2.2.1}/examples/dify/README.md +61 -7
- {pushary-2.2.0 → pushary-2.2.1}/pyproject.toml +1 -1
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/__init__.py +1 -1
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/client.py +3 -3
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/keys.py +4 -2
- {pushary-2.2.0 → pushary-2.2.1}/tests/test_client.py +2 -2
- {pushary-2.2.0 → pushary-2.2.1}/.gitignore +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/LICENSE +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/README.md +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/examples/agno/README.md +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/examples/llamaindex/README.md +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/examples/microsoft/README.md +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/examples/strands/README.md +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/_types.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/_util.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/adapters.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/decisions.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/errors.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/py.typed +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/src/pushary/webhook.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/tests/test_adapters.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/tests/test_agent.py +0 -0
- {pushary-2.2.0 → pushary-2.2.1}/tests/test_webhook.py +0 -0
|
@@ -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
|
|
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
|
|
111
|
-
The
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|