1m-handshake 0.1.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.
- 1m_handshake-0.1.0/1m_handshake.egg-info/PKG-INFO +62 -0
- 1m_handshake-0.1.0/1m_handshake.egg-info/SOURCES.txt +39 -0
- 1m_handshake-0.1.0/1m_handshake.egg-info/dependency_links.txt +1 -0
- 1m_handshake-0.1.0/1m_handshake.egg-info/entry_points.txt +2 -0
- 1m_handshake-0.1.0/1m_handshake.egg-info/requires.txt +10 -0
- 1m_handshake-0.1.0/1m_handshake.egg-info/top_level.txt +1 -0
- 1m_handshake-0.1.0/LICENSE +21 -0
- 1m_handshake-0.1.0/PKG-INFO +62 -0
- 1m_handshake-0.1.0/README.md +43 -0
- 1m_handshake-0.1.0/handshake_addon/__init__.py +0 -0
- 1m_handshake-0.1.0/handshake_addon/a2a_dispatch.py +94 -0
- 1m_handshake-0.1.0/handshake_addon/autonomous.py +80 -0
- 1m_handshake-0.1.0/handshake_addon/backend_client.py +292 -0
- 1m_handshake-0.1.0/handshake_addon/cli.py +222 -0
- 1m_handshake-0.1.0/handshake_addon/confirmation.py +121 -0
- 1m_handshake-0.1.0/handshake_addon/idp_discovery.py +109 -0
- 1m_handshake-0.1.0/handshake_addon/local_store.py +76 -0
- 1m_handshake-0.1.0/handshake_addon/on_demand.py +84 -0
- 1m_handshake-0.1.0/handshake_addon/reactive.py +249 -0
- 1m_handshake-0.1.0/handshake_addon/staging_x402_signer.py +87 -0
- 1m_handshake-0.1.0/handshake_addon/use_credit.py +43 -0
- 1m_handshake-0.1.0/handshake_addon/verification.py +253 -0
- 1m_handshake-0.1.0/handshake_addon/x402_client.py +43 -0
- 1m_handshake-0.1.0/pyproject.toml +42 -0
- 1m_handshake-0.1.0/setup.cfg +4 -0
- 1m_handshake-0.1.0/tests/test_a2a_dispatch.py +133 -0
- 1m_handshake-0.1.0/tests/test_autonomous.py +67 -0
- 1m_handshake-0.1.0/tests/test_backend_client.py +410 -0
- 1m_handshake-0.1.0/tests/test_cli_autonomy.py +21 -0
- 1m_handshake-0.1.0/tests/test_cli_install.py +133 -0
- 1m_handshake-0.1.0/tests/test_cli_on_demand.py +121 -0
- 1m_handshake-0.1.0/tests/test_cli_use_credit.py +90 -0
- 1m_handshake-0.1.0/tests/test_confirmation.py +103 -0
- 1m_handshake-0.1.0/tests/test_idp_discovery.py +135 -0
- 1m_handshake-0.1.0/tests/test_local_store.py +54 -0
- 1m_handshake-0.1.0/tests/test_on_demand.py +156 -0
- 1m_handshake-0.1.0/tests/test_reactive.py +319 -0
- 1m_handshake-0.1.0/tests/test_staging_x402_signer.py +56 -0
- 1m_handshake-0.1.0/tests/test_use_credit.py +65 -0
- 1m_handshake-0.1.0/tests/test_verification.py +544 -0
- 1m_handshake-0.1.0/tests/test_x402_client.py +42 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 1m-handshake
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client-side addon for creating verified 1M Agent Handshake registry claims
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: a2a-sdk<2,>=1.1
|
|
10
|
+
Requires-Dist: azure-identity<2,>=1.19
|
|
11
|
+
Requires-Dist: click<9,>=8.1
|
|
12
|
+
Requires-Dist: httpx<1,>=0.27
|
|
13
|
+
Requires-Dist: x402[evm]<3,>=2.18
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
17
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# 1M Handshake Addon
|
|
21
|
+
|
|
22
|
+
The free client-side addon for the 1M Agent Handshake registry. It connects your organization's A2A-capable agent to the registry and supplies proof when your agent makes a real call to another organization's public AgentCard.
|
|
23
|
+
|
|
24
|
+
Installing and using the addon is free. A fee is charged only when a credit is redeemed to create a permanent, public handshake record in the registry.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install 1m-handshake
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Set up your organization
|
|
33
|
+
|
|
34
|
+
Run the install command once for your organization. Supply the public 1M Handshake service URL provided for your environment:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
1m-handshake install \
|
|
38
|
+
--org-domain acme.example \
|
|
39
|
+
--backend-url https://<1m-handshake-service-url>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The addon first tries a configured enterprise agent identity. If that is unavailable, it guides you through one-time work-email or DNS-TXT domain verification.
|
|
43
|
+
|
|
44
|
+
## Create a handshake
|
|
45
|
+
|
|
46
|
+
After a credit has been purchased for a selected counterparty, redeem it by having your agent make a real A2A call:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
1m-handshake on-demand <credit-code>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You can also use an eligible prepaid credit for a chosen organization:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
1m-handshake use-credit counterparty.example
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The addon never stores wallet or payment-provider credentials. Autonomous x402 redemption is opt-in and requires a signer supplied by the host integration.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
1m_handshake.egg-info/PKG-INFO
|
|
5
|
+
1m_handshake.egg-info/SOURCES.txt
|
|
6
|
+
1m_handshake.egg-info/dependency_links.txt
|
|
7
|
+
1m_handshake.egg-info/entry_points.txt
|
|
8
|
+
1m_handshake.egg-info/requires.txt
|
|
9
|
+
1m_handshake.egg-info/top_level.txt
|
|
10
|
+
handshake_addon/__init__.py
|
|
11
|
+
handshake_addon/a2a_dispatch.py
|
|
12
|
+
handshake_addon/autonomous.py
|
|
13
|
+
handshake_addon/backend_client.py
|
|
14
|
+
handshake_addon/cli.py
|
|
15
|
+
handshake_addon/confirmation.py
|
|
16
|
+
handshake_addon/idp_discovery.py
|
|
17
|
+
handshake_addon/local_store.py
|
|
18
|
+
handshake_addon/on_demand.py
|
|
19
|
+
handshake_addon/reactive.py
|
|
20
|
+
handshake_addon/staging_x402_signer.py
|
|
21
|
+
handshake_addon/use_credit.py
|
|
22
|
+
handshake_addon/verification.py
|
|
23
|
+
handshake_addon/x402_client.py
|
|
24
|
+
tests/test_a2a_dispatch.py
|
|
25
|
+
tests/test_autonomous.py
|
|
26
|
+
tests/test_backend_client.py
|
|
27
|
+
tests/test_cli_autonomy.py
|
|
28
|
+
tests/test_cli_install.py
|
|
29
|
+
tests/test_cli_on_demand.py
|
|
30
|
+
tests/test_cli_use_credit.py
|
|
31
|
+
tests/test_confirmation.py
|
|
32
|
+
tests/test_idp_discovery.py
|
|
33
|
+
tests/test_local_store.py
|
|
34
|
+
tests/test_on_demand.py
|
|
35
|
+
tests/test_reactive.py
|
|
36
|
+
tests/test_staging_x402_signer.py
|
|
37
|
+
tests/test_use_credit.py
|
|
38
|
+
tests/test_verification.py
|
|
39
|
+
tests/test_x402_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
handshake_addon
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Freshstart Digital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 1m-handshake
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client-side addon for creating verified 1M Agent Handshake registry claims
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: a2a-sdk<2,>=1.1
|
|
10
|
+
Requires-Dist: azure-identity<2,>=1.19
|
|
11
|
+
Requires-Dist: click<9,>=8.1
|
|
12
|
+
Requires-Dist: httpx<1,>=0.27
|
|
13
|
+
Requires-Dist: x402[evm]<3,>=2.18
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
17
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# 1M Handshake Addon
|
|
21
|
+
|
|
22
|
+
The free client-side addon for the 1M Agent Handshake registry. It connects your organization's A2A-capable agent to the registry and supplies proof when your agent makes a real call to another organization's public AgentCard.
|
|
23
|
+
|
|
24
|
+
Installing and using the addon is free. A fee is charged only when a credit is redeemed to create a permanent, public handshake record in the registry.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install 1m-handshake
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Set up your organization
|
|
33
|
+
|
|
34
|
+
Run the install command once for your organization. Supply the public 1M Handshake service URL provided for your environment:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
1m-handshake install \
|
|
38
|
+
--org-domain acme.example \
|
|
39
|
+
--backend-url https://<1m-handshake-service-url>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The addon first tries a configured enterprise agent identity. If that is unavailable, it guides you through one-time work-email or DNS-TXT domain verification.
|
|
43
|
+
|
|
44
|
+
## Create a handshake
|
|
45
|
+
|
|
46
|
+
After a credit has been purchased for a selected counterparty, redeem it by having your agent make a real A2A call:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
1m-handshake on-demand <credit-code>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You can also use an eligible prepaid credit for a chosen organization:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
1m-handshake use-credit counterparty.example
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The addon never stores wallet or payment-provider credentials. Autonomous x402 redemption is opt-in and requires a signer supplied by the host integration.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 1M Handshake Addon
|
|
2
|
+
|
|
3
|
+
The free client-side addon for the 1M Agent Handshake registry. It connects your organization's A2A-capable agent to the registry and supplies proof when your agent makes a real call to another organization's public AgentCard.
|
|
4
|
+
|
|
5
|
+
Installing and using the addon is free. A fee is charged only when a credit is redeemed to create a permanent, public handshake record in the registry.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install 1m-handshake
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Set up your organization
|
|
14
|
+
|
|
15
|
+
Run the install command once for your organization. Supply the public 1M Handshake service URL provided for your environment:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
1m-handshake install \
|
|
19
|
+
--org-domain acme.example \
|
|
20
|
+
--backend-url https://<1m-handshake-service-url>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The addon first tries a configured enterprise agent identity. If that is unavailable, it guides you through one-time work-email or DNS-TXT domain verification.
|
|
24
|
+
|
|
25
|
+
## Create a handshake
|
|
26
|
+
|
|
27
|
+
After a credit has been purchased for a selected counterparty, redeem it by having your agent make a real A2A call:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
1m-handshake on-demand <credit-code>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also use an eligible prepaid credit for a chosen organization:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
1m-handshake use-credit counterparty.example
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The addon never stores wallet or payment-provider credentials. Autonomous x402 redemption is opt-in and requires a signer supplied by the host integration.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT. See [LICENSE](LICENSE).
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# handshake_addon/a2a_dispatch.py
|
|
2
|
+
"""On-demand mode's active call origination.
|
|
3
|
+
|
|
4
|
+
Unlike reactive mode (a separate, later plan), which only *observes* calls
|
|
5
|
+
an org's own agent already made via an SDK interceptor, on-demand mode
|
|
6
|
+
requires the addon itself to *originate* a real SendMessage call — the
|
|
7
|
+
owner explicitly paid for this specific call to happen right now.
|
|
8
|
+
|
|
9
|
+
The A2A SDK's message-sending surface is protobuf-based, confirmed against
|
|
10
|
+
the actually-installed a2a-sdk==1.1.2 (not assumed): `ClientFactory` /
|
|
11
|
+
`ClientConfig` come from `a2a.client`; `Message`/`Part`/`Role`/
|
|
12
|
+
`SendMessageRequest`/`StreamResponse`/`Task`/`TaskStatus`/`TaskState` come
|
|
13
|
+
from `a2a.types.a2a_pb2`. `ClientFactory(config).create_from_url(url)`
|
|
14
|
+
fetches `<url>/.well-known/agent-card.json` automatically.
|
|
15
|
+
"""
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from a2a.client import ClientConfig, ClientFactory
|
|
21
|
+
from a2a.types.a2a_pb2 import Message, Part, Role, SendMessageRequest, TaskState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DispatchFailed(Exception):
|
|
25
|
+
"""The addon could not complete a real SendMessage call to the counterparty."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_FAILURE_STATES = {"failed", "rejected", "canceled", "unspecified", "unknown"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _task_status_from_response(response) -> str | None:
|
|
32
|
+
"""Returns the task's state if this response carries one, else None.
|
|
33
|
+
|
|
34
|
+
`message`/`artifact_update` events (and an unset oneof) carry no task
|
|
35
|
+
state — they must not overwrite a real terminal state seen earlier in
|
|
36
|
+
the stream, and must not be reported as a task outcome themselves.
|
|
37
|
+
"""
|
|
38
|
+
which = response.WhichOneof("payload")
|
|
39
|
+
if which == "task":
|
|
40
|
+
return TaskState.Name(response.task.status.state).removeprefix("TASK_STATE_").lower()
|
|
41
|
+
if which == "status_update":
|
|
42
|
+
return TaskState.Name(response.status_update.status.state).removeprefix("TASK_STATE_").lower()
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def dispatch_call(org_b_domain: str) -> dict:
|
|
47
|
+
"""Actively originates a real A2A SendMessage call to org_b_domain.
|
|
48
|
+
|
|
49
|
+
Returns a dict matching the backend's CallProof schema (method,
|
|
50
|
+
target_domain, status, timestamp) built from what actually happened —
|
|
51
|
+
never fabricated, never assumed successful. Raises DispatchFailed for
|
|
52
|
+
any failure: unreachable AgentCard, a failed send, or no response at
|
|
53
|
+
all.
|
|
54
|
+
"""
|
|
55
|
+
async with httpx.AsyncClient(timeout=15.0) as http_client:
|
|
56
|
+
factory = ClientFactory(ClientConfig(httpx_client=http_client, streaming=False))
|
|
57
|
+
try:
|
|
58
|
+
client = await factory.create_from_url(f"https://{org_b_domain}")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise DispatchFailed(f"could not resolve {org_b_domain}'s AgentCard: {e}") from e
|
|
61
|
+
|
|
62
|
+
request = SendMessageRequest(
|
|
63
|
+
message=Message(
|
|
64
|
+
message_id=str(uuid.uuid4()),
|
|
65
|
+
role=Role.ROLE_USER,
|
|
66
|
+
parts=[Part(text="1M Agent Handshake — on-demand verification call.")],
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
last_response = None
|
|
70
|
+
last_task_status = None
|
|
71
|
+
try:
|
|
72
|
+
async for response in client.send_message(request):
|
|
73
|
+
last_response = response
|
|
74
|
+
status = _task_status_from_response(response)
|
|
75
|
+
if status is not None:
|
|
76
|
+
last_task_status = status
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise DispatchFailed(f"SendMessage call to {org_b_domain} failed: {e}") from e
|
|
79
|
+
finally:
|
|
80
|
+
await client.close()
|
|
81
|
+
|
|
82
|
+
if last_response is None:
|
|
83
|
+
raise DispatchFailed(f"{org_b_domain} returned no response to the SendMessage call")
|
|
84
|
+
|
|
85
|
+
status = last_task_status if last_task_status is not None else "unknown"
|
|
86
|
+
if status in _FAILURE_STATES:
|
|
87
|
+
raise DispatchFailed(f"{org_b_domain} reported the call did not complete (status={status!r})")
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"method": "send_message",
|
|
91
|
+
"target_domain": org_b_domain,
|
|
92
|
+
"status": status,
|
|
93
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
94
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Bounded, recoverable autonomous x402 claims for observed reactive proofs."""
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from x402.schemas import PaymentRequired
|
|
8
|
+
|
|
9
|
+
from handshake_addon import backend_client
|
|
10
|
+
from handshake_addon.local_store import LocalStore
|
|
11
|
+
from handshake_addon.x402_client import PaymentSignerProvider, SignerUnavailable, create_payment_payload
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _key(org_b: str, org_a: str) -> str:
|
|
15
|
+
return f"autonomous_charge:{org_b}:{org_a}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def policy_key(org_a: str) -> str:
|
|
19
|
+
return f"autonomy_policy:{org_a}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_policy(store: LocalStore, org_a: str) -> dict | None:
|
|
23
|
+
raw = store.get(policy_key(org_a))
|
|
24
|
+
try:
|
|
25
|
+
return json.loads(raw) if raw else None
|
|
26
|
+
except json.JSONDecodeError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AutonomousResult:
|
|
32
|
+
status: str # succeeded | pending | unavailable | failed
|
|
33
|
+
message: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def attempt_claim(*, backend_base_url: str, org_a_domain: str, org_b_domain: str,
|
|
37
|
+
description: str, proof: dict, store: LocalStore,
|
|
38
|
+
signer_provider: PaymentSignerProvider | None) -> AutonomousResult:
|
|
39
|
+
policy = get_policy(store, org_a_domain)
|
|
40
|
+
if policy is None or signer_provider is None:
|
|
41
|
+
return AutonomousResult("unavailable", "")
|
|
42
|
+
key = _key(org_b_domain, org_a_domain)
|
|
43
|
+
prior_raw = store.get(key)
|
|
44
|
+
if prior_raw:
|
|
45
|
+
try:
|
|
46
|
+
prior = json.loads(prior_raw)
|
|
47
|
+
attempt_id = prior.get("attempt_id")
|
|
48
|
+
if attempt_id:
|
|
49
|
+
info = await backend_client.get_checkout_attempt(backend_base_url, attempt_id)
|
|
50
|
+
if info.status == "settled" and info.approval_code:
|
|
51
|
+
result = await backend_client.redeem_code(backend_base_url, info.approval_code.code, proof)
|
|
52
|
+
prior.update(status="succeeded", code=info.approval_code.code)
|
|
53
|
+
store.set(key, json.dumps(prior))
|
|
54
|
+
return AutonomousResult("succeeded", f"I already claimed this interaction as handshake #{result.sequence_number}.")
|
|
55
|
+
if info.status == "settling":
|
|
56
|
+
return AutonomousResult("pending", "I'm handling this handshake; check back shortly.")
|
|
57
|
+
except Exception:
|
|
58
|
+
return AutonomousResult("pending", "I'm handling this handshake; check back shortly.")
|
|
59
|
+
attempt_id = str(uuid.uuid4())
|
|
60
|
+
audit = {"status": "attempted", "attempt_id": attempt_id, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
61
|
+
store.set(key, json.dumps(audit))
|
|
62
|
+
request = {"org_a_domain": org_a_domain, "org_b_domain": org_b_domain, "description": description, "trigger_mode": "reactive"}
|
|
63
|
+
try:
|
|
64
|
+
challenge = await backend_client.start_x402_checkout(backend_base_url, request, attempt_id)
|
|
65
|
+
if challenge.amount_cents > int(policy["max_cents"]):
|
|
66
|
+
audit.update(status="failed", amount_cents=challenge.amount_cents)
|
|
67
|
+
store.set(key, json.dumps(audit))
|
|
68
|
+
return AutonomousResult("failed", "")
|
|
69
|
+
payload = await create_payment_payload(PaymentRequired.model_validate(challenge.x402_requirements), signer_provider)
|
|
70
|
+
info = await backend_client.submit_x402_payment(backend_base_url, request, attempt_id, payload)
|
|
71
|
+
if info.status != "settled" or not info.approval_code:
|
|
72
|
+
return AutonomousResult("pending", "I'm handling this handshake; check back shortly.")
|
|
73
|
+
result = await backend_client.redeem_code(backend_base_url, info.approval_code.code, proof)
|
|
74
|
+
audit.update(status="succeeded", amount_cents=challenge.amount_cents, code=info.approval_code.code)
|
|
75
|
+
store.set(key, json.dumps(audit))
|
|
76
|
+
return AutonomousResult("succeeded", f"I already claimed this interaction as handshake #{result.sequence_number}.")
|
|
77
|
+
except SignerUnavailable:
|
|
78
|
+
return AutonomousResult("unavailable", "")
|
|
79
|
+
except Exception:
|
|
80
|
+
return AutonomousResult("pending", "I'm handling this handshake; check back shortly.")
|