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.
Files changed (41) hide show
  1. 1m_handshake-0.1.0/1m_handshake.egg-info/PKG-INFO +62 -0
  2. 1m_handshake-0.1.0/1m_handshake.egg-info/SOURCES.txt +39 -0
  3. 1m_handshake-0.1.0/1m_handshake.egg-info/dependency_links.txt +1 -0
  4. 1m_handshake-0.1.0/1m_handshake.egg-info/entry_points.txt +2 -0
  5. 1m_handshake-0.1.0/1m_handshake.egg-info/requires.txt +10 -0
  6. 1m_handshake-0.1.0/1m_handshake.egg-info/top_level.txt +1 -0
  7. 1m_handshake-0.1.0/LICENSE +21 -0
  8. 1m_handshake-0.1.0/PKG-INFO +62 -0
  9. 1m_handshake-0.1.0/README.md +43 -0
  10. 1m_handshake-0.1.0/handshake_addon/__init__.py +0 -0
  11. 1m_handshake-0.1.0/handshake_addon/a2a_dispatch.py +94 -0
  12. 1m_handshake-0.1.0/handshake_addon/autonomous.py +80 -0
  13. 1m_handshake-0.1.0/handshake_addon/backend_client.py +292 -0
  14. 1m_handshake-0.1.0/handshake_addon/cli.py +222 -0
  15. 1m_handshake-0.1.0/handshake_addon/confirmation.py +121 -0
  16. 1m_handshake-0.1.0/handshake_addon/idp_discovery.py +109 -0
  17. 1m_handshake-0.1.0/handshake_addon/local_store.py +76 -0
  18. 1m_handshake-0.1.0/handshake_addon/on_demand.py +84 -0
  19. 1m_handshake-0.1.0/handshake_addon/reactive.py +249 -0
  20. 1m_handshake-0.1.0/handshake_addon/staging_x402_signer.py +87 -0
  21. 1m_handshake-0.1.0/handshake_addon/use_credit.py +43 -0
  22. 1m_handshake-0.1.0/handshake_addon/verification.py +253 -0
  23. 1m_handshake-0.1.0/handshake_addon/x402_client.py +43 -0
  24. 1m_handshake-0.1.0/pyproject.toml +42 -0
  25. 1m_handshake-0.1.0/setup.cfg +4 -0
  26. 1m_handshake-0.1.0/tests/test_a2a_dispatch.py +133 -0
  27. 1m_handshake-0.1.0/tests/test_autonomous.py +67 -0
  28. 1m_handshake-0.1.0/tests/test_backend_client.py +410 -0
  29. 1m_handshake-0.1.0/tests/test_cli_autonomy.py +21 -0
  30. 1m_handshake-0.1.0/tests/test_cli_install.py +133 -0
  31. 1m_handshake-0.1.0/tests/test_cli_on_demand.py +121 -0
  32. 1m_handshake-0.1.0/tests/test_cli_use_credit.py +90 -0
  33. 1m_handshake-0.1.0/tests/test_confirmation.py +103 -0
  34. 1m_handshake-0.1.0/tests/test_idp_discovery.py +135 -0
  35. 1m_handshake-0.1.0/tests/test_local_store.py +54 -0
  36. 1m_handshake-0.1.0/tests/test_on_demand.py +156 -0
  37. 1m_handshake-0.1.0/tests/test_reactive.py +319 -0
  38. 1m_handshake-0.1.0/tests/test_staging_x402_signer.py +56 -0
  39. 1m_handshake-0.1.0/tests/test_use_credit.py +65 -0
  40. 1m_handshake-0.1.0/tests/test_verification.py +544 -0
  41. 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,2 @@
1
+ [console_scripts]
2
+ 1m-handshake = handshake_addon.cli:cli
@@ -0,0 +1,10 @@
1
+ a2a-sdk<2,>=1.1
2
+ azure-identity<2,>=1.19
3
+ click<9,>=8.1
4
+ httpx<1,>=0.27
5
+ x402[evm]<3,>=2.18
6
+
7
+ [dev]
8
+ pytest>=8.3
9
+ pytest-asyncio>=0.24
10
+ respx>=0.21
@@ -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.")