karma-openclaw 0.4.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 (38) hide show
  1. karma_openclaw-0.4.0/PKG-INFO +114 -0
  2. karma_openclaw-0.4.0/README.md +100 -0
  3. karma_openclaw-0.4.0/karma_openclaw/__init__.py +21 -0
  4. karma_openclaw-0.4.0/karma_openclaw/__main__.py +6 -0
  5. karma_openclaw-0.4.0/karma_openclaw/agent_binding.py +180 -0
  6. karma_openclaw-0.4.0/karma_openclaw/agent_env.py +121 -0
  7. karma_openclaw-0.4.0/karma_openclaw/bilateral_tools.py +104 -0
  8. karma_openclaw-0.4.0/karma_openclaw/guard.py +152 -0
  9. karma_openclaw-0.4.0/karma_openclaw/handoff.py +102 -0
  10. karma_openclaw-0.4.0/karma_openclaw/helpers.py +145 -0
  11. karma_openclaw-0.4.0/karma_openclaw/http_client.py +229 -0
  12. karma_openclaw-0.4.0/karma_openclaw/orchestration.py +70 -0
  13. karma_openclaw-0.4.0/karma_openclaw/p0_tools.py +292 -0
  14. karma_openclaw-0.4.0/karma_openclaw/pairing_tools.py +628 -0
  15. karma_openclaw-0.4.0/karma_openclaw/phase1_tools.py +301 -0
  16. karma_openclaw-0.4.0/karma_openclaw/phase2_tools.py +33 -0
  17. karma_openclaw-0.4.0/karma_openclaw/runtime_tools.py +192 -0
  18. karma_openclaw-0.4.0/karma_openclaw/server.py +275 -0
  19. karma_openclaw-0.4.0/karma_openclaw.egg-info/PKG-INFO +114 -0
  20. karma_openclaw-0.4.0/karma_openclaw.egg-info/SOURCES.txt +36 -0
  21. karma_openclaw-0.4.0/karma_openclaw.egg-info/dependency_links.txt +1 -0
  22. karma_openclaw-0.4.0/karma_openclaw.egg-info/entry_points.txt +2 -0
  23. karma_openclaw-0.4.0/karma_openclaw.egg-info/requires.txt +7 -0
  24. karma_openclaw-0.4.0/karma_openclaw.egg-info/top_level.txt +1 -0
  25. karma_openclaw-0.4.0/pyproject.toml +26 -0
  26. karma_openclaw-0.4.0/setup.cfg +4 -0
  27. karma_openclaw-0.4.0/tests/test_agent_binding.py +143 -0
  28. karma_openclaw-0.4.0/tests/test_agent_env_live.py +87 -0
  29. karma_openclaw-0.4.0/tests/test_automation_readiness_mcp.py +38 -0
  30. karma_openclaw-0.4.0/tests/test_dev_delivery_signatures.py +15 -0
  31. karma_openclaw-0.4.0/tests/test_guard.py +48 -0
  32. karma_openclaw-0.4.0/tests/test_handoff.py +44 -0
  33. karma_openclaw-0.4.0/tests/test_helpers.py +44 -0
  34. karma_openclaw-0.4.0/tests/test_import.py +12 -0
  35. karma_openclaw-0.4.0/tests/test_openclaw_server_identity.py +60 -0
  36. karma_openclaw-0.4.0/tests/test_orchestration.py +16 -0
  37. karma_openclaw-0.4.0/tests/test_pairing_mcp.py +34 -0
  38. karma_openclaw-0.4.0/tests/test_phase1_tools.py +20 -0
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: karma-openclaw
3
+ Version: 0.4.0
4
+ Summary: Karma ↔ OpenClaw — stdio MCP server bridging to Karma public HTTP API
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cryptography>=42.0.0
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: mcp<2,>=1.8.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.2.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
14
+
15
+ # karma-openclaw — OpenClaw MCP proof plugin
16
+
17
+ Stdio MCP server that attaches verifiable execution receipts and evidence bundles to OpenClaw agent workflows. Karma acts as a proof layer for paid agent actions — it does not replace payment or tool execution, it records proof after actions happen.
18
+
19
+ **P0 proof tools:** execution receipt construction, evidence bundle submission, verification and handoff validation. High-risk settlement actions (voucher create/accept, Runtime Key mint) remain manual in the Karma Console. See [Advanced OpenClaw Workflows](../../docs/OPENCLAW_P1_DUAL_AGENT.md) and [`examples/openclaw-dual-agent/`](../../examples/openclaw-dual-agent/).
20
+
21
+ **Runtime Key access is agent-bound.** A `KRM_RT_…` key is a bearer token — whoever holds it could spend the owner's money — so Karma mints every key against a named agent, pins it to that agent's Ed25519 public key and requires the agent to sign every `/runtime/*` request with the matching private key. The private key never leaves the agent machine, so a copied key string is useless.
22
+
23
+ **Pairing activates the key by itself.** `karma_connect` generates this machine's Ed25519 key on first use and sends its public key with the pairing request, so the owner's approval *is* the activation: `karma_connect_claim` returns a key that is already live — nothing to type, no matching code. (Without a local key, or if the declared key is malformed, the older two-step path still applies: `karma_runtime_bind_key` hands the owner an 8-character `activation_code`, then `karma_runtime_await_activation`.) Keys have no fixed expiry by default; the owner revokes them in the Console. Set `KARMA_RUNTIME_KEY`, `KARMA_AGENT_ID` and `KARMA_AGENT_PRIVATE_KEY` (Ed25519, stays on this machine). See [Runtime Key 指南](../../docs/runtime-key-guide.md).
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install ./packages/karma-openclaw
31
+ # or dev mode:
32
+ pip install -e ./packages/karma-openclaw
33
+ # no clone? one line, from the public repo:
34
+ pip install "karma-openclaw @ git+https://github.com/AtoB101/Karma@main#subdirectory=packages/karma-openclaw"
35
+ ```
36
+
37
+ Not on PyPI yet (`pip install karma-openclaw` returns 404); see
38
+ [`docs/mcp/karma-mcp-integration.md`](../../docs/mcp/karma-mcp-integration.md).
39
+
40
+ ## Run (stdio MCP)
41
+
42
+ ```bash
43
+ export KARMA_RUNTIME_URL=http://localhost:8000
44
+ export KARMA_API_KEY=***karma_worker-001_***
45
+ export KARMA_OPENCLAW_HANDOFF_PATH=./handoff.json
46
+
47
+ # /runtime/* tools (agent-bound key):
48
+ export KARMA_RUNTIME_KEY=KRM_RT_…
49
+ export KARMA_AGENT_ID=<the agent this key names>
50
+ export KARMA_AGENT_PRIVATE_KEY=<base64(32 bytes) or 64-char hex>
51
+
52
+ karma-openclaw-mcp
53
+ ```
54
+
55
+ ## MCP Proof Tools
56
+
57
+ | Tool | Purpose |
58
+ |------|---------|
59
+ | `karma_build_execution_receipt_step` | Generate a signed receipt for one tool call |
60
+ | `karma_submit_evidence_bundle` | Package receipts into a verifiable bundle |
61
+ | `karma_get_evidence_bundle` | Retrieve a previously submitted bundle |
62
+ | `karma_validate_handoff` | Verify operator handoff for high-risk actions |
63
+ | `karma_connect` | **No key yet?** Ask to be connected; returns the `user_code` for your owner |
64
+ | `karma_connect_claim` | Collect the credentials as soon as the owner approves; writes `~/.karma/agent.env` (0600) — including the local Ed25519 seed — and returns fingerprints only. Also answers "where is this pairing now": while it is unapproved it returns status only and consumes nothing |
65
+ | `karma_connect_status` | What this machine holds: pending pairings + credential fingerprints. Local only — it never calls the backend |
66
+ | `karma_pairing_*` | Deprecated aliases of the three above (same code path, each return carries a `deprecated` note). **Removal date: 2026-12-01** (next minor release); see `docs/AGENT_PAIRING_V1.md` |
67
+ | `karma_runtime_bind_key` | Bind this agent's Ed25519 public key; returns the matching code for the owner |
68
+ | `karma_runtime_await_activation` | Wait for the owner to enter the code; then signs every request |
69
+ | `karma_runtime_binding_status` | Where this key stands (pending / active / bearer) |
70
+ | `karma_check_automation_readiness` | Check if automation policy allows this action |
71
+ | `karma_get_settlement` | (Optional) Read settlement status |
72
+
73
+ ## Onboarding with no key at all
74
+
75
+ An agent that holds nothing can still get itself connected — and the credentials never
76
+ travel through the chat:
77
+
78
+ ```text
79
+ karma_connect(agent_name="claw-001", requested_side="seller", requested_vertical="food")
80
+ -> user_code "QKFS-3J5Z" + verification_uri + agent_fingerprint
81
+ # hand these to your owner: they compare agent_fingerprint with the Console card,
82
+ # so "the request in the Console" is provably "this machine's public key"
83
+ ... owner reviews it in Karma Console, sets the allowance, and approves — that is the delivery
84
+ karma_connect_claim()
85
+ -> credentials written to ~/.karma/agent.env (0600); only sha256 fingerprints returned
86
+ karma_connect_status()
87
+ -> what this machine holds; local only, no network
88
+ ```
89
+
90
+ The direction is fixed: the agent produces the code, the owner types it in, approval releases
91
+ the credentials. The agent's own `pairing_code` (256-bit, returned once, stored as SHA-256) is
92
+ the only thing that redeems them — so a `user_code` on a screenshot, or a whole chat transcript,
93
+ is worth nothing. If the owner wants one more hand-to-hand confirmation they can *optionally*
94
+ issue a `handoff_code` (3 minutes, single use); then — and only then — the claim has to carry
95
+ it too. See [Agent 配对接入 v1](../../docs/AGENT_PAIRING_V1.md).
96
+
97
+ ## Quick Demo
98
+
99
+ ```bash
100
+ pip install -e ".[dev]"
101
+ uvicorn api.app:app --reload &
102
+ export KARMA_RUNTIME_URL=http://127.0.0.1:8000
103
+ export KARMA_API_KEY=***
104
+
105
+ karma-openclaw-mcp
106
+ ```
107
+
108
+ Then connect OpenClaw to the MCP server and call `karma_build_execution_receipt_step`.
109
+
110
+ ---
111
+
112
+ ## License
113
+
114
+ AGPL-3.0-only — see [LICENSE](../../LICENSE).
@@ -0,0 +1,100 @@
1
+ # karma-openclaw — OpenClaw MCP proof plugin
2
+
3
+ Stdio MCP server that attaches verifiable execution receipts and evidence bundles to OpenClaw agent workflows. Karma acts as a proof layer for paid agent actions — it does not replace payment or tool execution, it records proof after actions happen.
4
+
5
+ **P0 proof tools:** execution receipt construction, evidence bundle submission, verification and handoff validation. High-risk settlement actions (voucher create/accept, Runtime Key mint) remain manual in the Karma Console. See [Advanced OpenClaw Workflows](../../docs/OPENCLAW_P1_DUAL_AGENT.md) and [`examples/openclaw-dual-agent/`](../../examples/openclaw-dual-agent/).
6
+
7
+ **Runtime Key access is agent-bound.** A `KRM_RT_…` key is a bearer token — whoever holds it could spend the owner's money — so Karma mints every key against a named agent, pins it to that agent's Ed25519 public key and requires the agent to sign every `/runtime/*` request with the matching private key. The private key never leaves the agent machine, so a copied key string is useless.
8
+
9
+ **Pairing activates the key by itself.** `karma_connect` generates this machine's Ed25519 key on first use and sends its public key with the pairing request, so the owner's approval *is* the activation: `karma_connect_claim` returns a key that is already live — nothing to type, no matching code. (Without a local key, or if the declared key is malformed, the older two-step path still applies: `karma_runtime_bind_key` hands the owner an 8-character `activation_code`, then `karma_runtime_await_activation`.) Keys have no fixed expiry by default; the owner revokes them in the Console. Set `KARMA_RUNTIME_KEY`, `KARMA_AGENT_ID` and `KARMA_AGENT_PRIVATE_KEY` (Ed25519, stays on this machine). See [Runtime Key 指南](../../docs/runtime-key-guide.md).
10
+
11
+ ---
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install ./packages/karma-openclaw
17
+ # or dev mode:
18
+ pip install -e ./packages/karma-openclaw
19
+ # no clone? one line, from the public repo:
20
+ pip install "karma-openclaw @ git+https://github.com/AtoB101/Karma@main#subdirectory=packages/karma-openclaw"
21
+ ```
22
+
23
+ Not on PyPI yet (`pip install karma-openclaw` returns 404); see
24
+ [`docs/mcp/karma-mcp-integration.md`](../../docs/mcp/karma-mcp-integration.md).
25
+
26
+ ## Run (stdio MCP)
27
+
28
+ ```bash
29
+ export KARMA_RUNTIME_URL=http://localhost:8000
30
+ export KARMA_API_KEY=***karma_worker-001_***
31
+ export KARMA_OPENCLAW_HANDOFF_PATH=./handoff.json
32
+
33
+ # /runtime/* tools (agent-bound key):
34
+ export KARMA_RUNTIME_KEY=KRM_RT_…
35
+ export KARMA_AGENT_ID=<the agent this key names>
36
+ export KARMA_AGENT_PRIVATE_KEY=<base64(32 bytes) or 64-char hex>
37
+
38
+ karma-openclaw-mcp
39
+ ```
40
+
41
+ ## MCP Proof Tools
42
+
43
+ | Tool | Purpose |
44
+ |------|---------|
45
+ | `karma_build_execution_receipt_step` | Generate a signed receipt for one tool call |
46
+ | `karma_submit_evidence_bundle` | Package receipts into a verifiable bundle |
47
+ | `karma_get_evidence_bundle` | Retrieve a previously submitted bundle |
48
+ | `karma_validate_handoff` | Verify operator handoff for high-risk actions |
49
+ | `karma_connect` | **No key yet?** Ask to be connected; returns the `user_code` for your owner |
50
+ | `karma_connect_claim` | Collect the credentials as soon as the owner approves; writes `~/.karma/agent.env` (0600) — including the local Ed25519 seed — and returns fingerprints only. Also answers "where is this pairing now": while it is unapproved it returns status only and consumes nothing |
51
+ | `karma_connect_status` | What this machine holds: pending pairings + credential fingerprints. Local only — it never calls the backend |
52
+ | `karma_pairing_*` | Deprecated aliases of the three above (same code path, each return carries a `deprecated` note). **Removal date: 2026-12-01** (next minor release); see `docs/AGENT_PAIRING_V1.md` |
53
+ | `karma_runtime_bind_key` | Bind this agent's Ed25519 public key; returns the matching code for the owner |
54
+ | `karma_runtime_await_activation` | Wait for the owner to enter the code; then signs every request |
55
+ | `karma_runtime_binding_status` | Where this key stands (pending / active / bearer) |
56
+ | `karma_check_automation_readiness` | Check if automation policy allows this action |
57
+ | `karma_get_settlement` | (Optional) Read settlement status |
58
+
59
+ ## Onboarding with no key at all
60
+
61
+ An agent that holds nothing can still get itself connected — and the credentials never
62
+ travel through the chat:
63
+
64
+ ```text
65
+ karma_connect(agent_name="claw-001", requested_side="seller", requested_vertical="food")
66
+ -> user_code "QKFS-3J5Z" + verification_uri + agent_fingerprint
67
+ # hand these to your owner: they compare agent_fingerprint with the Console card,
68
+ # so "the request in the Console" is provably "this machine's public key"
69
+ ... owner reviews it in Karma Console, sets the allowance, and approves — that is the delivery
70
+ karma_connect_claim()
71
+ -> credentials written to ~/.karma/agent.env (0600); only sha256 fingerprints returned
72
+ karma_connect_status()
73
+ -> what this machine holds; local only, no network
74
+ ```
75
+
76
+ The direction is fixed: the agent produces the code, the owner types it in, approval releases
77
+ the credentials. The agent's own `pairing_code` (256-bit, returned once, stored as SHA-256) is
78
+ the only thing that redeems them — so a `user_code` on a screenshot, or a whole chat transcript,
79
+ is worth nothing. If the owner wants one more hand-to-hand confirmation they can *optionally*
80
+ issue a `handoff_code` (3 minutes, single use); then — and only then — the claim has to carry
81
+ it too. See [Agent 配对接入 v1](../../docs/AGENT_PAIRING_V1.md).
82
+
83
+ ## Quick Demo
84
+
85
+ ```bash
86
+ pip install -e ".[dev]"
87
+ uvicorn api.app:app --reload &
88
+ export KARMA_RUNTIME_URL=http://127.0.0.1:8000
89
+ export KARMA_API_KEY=***
90
+
91
+ karma-openclaw-mcp
92
+ ```
93
+
94
+ Then connect OpenClaw to the MCP server and call `karma_build_execution_receipt_step`.
95
+
96
+ ---
97
+
98
+ ## License
99
+
100
+ AGPL-3.0-only — see [LICENSE](../../LICENSE).
@@ -0,0 +1,21 @@
1
+ """Karma ↔ OpenClaw MCP bridge package.
2
+
3
+ Keep package import light: handoff/helpers must work without pulling FastMCP
4
+ (mcp v1). Server entrypoints are exported lazily.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ __all__ = ["build_app", "main"]
12
+
13
+ __version__ = "0.4.0"
14
+
15
+
16
+ def __getattr__(name: str) -> Any:
17
+ if name in ("build_app", "main"):
18
+ from karma_openclaw.server import build_app, main
19
+
20
+ return build_app if name == "build_app" else main
21
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m karma_openclaw``."""
2
+
3
+ from karma_openclaw.server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,180 @@
1
+ """Agent-side Runtime Key binding: the Ed25519 key + the per-request signature.
2
+
3
+ Why this exists. A Runtime Key is a bearer token: whoever holds ``KRM_RT_...`` can spend
4
+ the owner's money within its limits. Karma therefore refuses to serve *any* key that is
5
+ not bound to an agent public key and activated by the owner with the 8-character
6
+ matching code. So an agent process must be able to
7
+
8
+ (a) present its Ed25519 public key (``POST /runtime/bind-key``), and
9
+ (b) sign every ``/runtime/*`` request (``X-Karma-Agent-Signature`` + timestamp + nonce).
10
+
11
+ This mirrors ``sdk/runtime_client.py`` and, on the server side,
12
+ ``services/runtime_wallet.build_agent_request_message``. Both sides are pinned against
13
+ each other by tests so the format cannot drift.
14
+
15
+ Env:
16
+ KARMA_AGENT_PRIVATE_KEY base64(32 raw bytes) or 64-char hex -- never leaves this process
17
+ KARMA_AGENT_ID the agent id the owner authorized (falls back to what the key was minted for)
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import hashlib
23
+ import os
24
+ import uuid
25
+ from datetime import UTC, datetime, timezone
26
+ from typing import Any
27
+
28
+
29
+ def runtime_key_id(token: str) -> str:
30
+ """Pull the key_id out of ``KRM_RT_<key_id>_<secret>`` (the signature message needs it)."""
31
+ raw = (token or "").strip()
32
+ if not raw.startswith("KRM_RT_"):
33
+ return ""
34
+ head, _, _ = raw[len("KRM_RT_") :].partition("_")
35
+ return head
36
+
37
+
38
+ def agent_key_from_seed(seed: str):
39
+ """base64(raw 32 bytes) or 64-char hex -> Ed25519PrivateKey."""
40
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
41
+
42
+ text = (seed or "").strip()
43
+ if not text:
44
+ raise ValueError("agent private key is empty")
45
+ if len(text) == 64 and all(c in "0123456789abcdefABCDEF" for c in text):
46
+ raw = bytes.fromhex(text)
47
+ else:
48
+ raw = base64.b64decode(text, validate=True)
49
+ if len(raw) != 32:
50
+ raise ValueError("agent private key must be 32 raw bytes (base64 or hex seed)")
51
+ return Ed25519PrivateKey.from_private_bytes(raw)
52
+
53
+
54
+ def agent_key_from_env() -> Any | None:
55
+ """The agent's Ed25519 key, or None when no seed is configured.
56
+
57
+ Reads the env var first, then the on-disk credential file -- the same file
58
+ ``karma_connect_claim`` writes, so a freshly paired process can sign without
59
+ waiting for its host to restart it.
60
+ """
61
+ from karma_openclaw.agent_env import credential
62
+
63
+ seed = credential("KARMA_AGENT_PRIVATE_KEY")
64
+ if not seed:
65
+ return None
66
+ return agent_key_from_seed(seed)
67
+
68
+
69
+ def ensure_local_agent_key() -> Any | None:
70
+ """本机 agent 私钥:有就用,没有就现生成一把并落盘(0600)。
71
+
72
+ 为什么由 agent 自己生成:这把私钥是「光有钥匙字符串花不了钱」的全部依据,
73
+ 它必须只存在于 agent 本机 —— Karma 服务端从头到尾看不到它,只收到公钥,
74
+ 用来把 Runtime Key 钉在这次的 agent 身上。
75
+ """
76
+ existing = agent_key_from_env()
77
+ if existing is not None:
78
+ return existing
79
+ try:
80
+ import base64 as _b64
81
+
82
+ from cryptography.hazmat.primitives import serialization
83
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
84
+
85
+ from karma_openclaw.agent_env import upsert_credential
86
+
87
+ key = Ed25519PrivateKey.generate()
88
+ raw = key.private_bytes(
89
+ serialization.Encoding.Raw,
90
+ serialization.PrivateFormat.Raw,
91
+ serialization.NoEncryption(),
92
+ )
93
+ upsert_credential("KARMA_AGENT_PRIVATE_KEY", _b64.b64encode(raw).decode())
94
+ return key
95
+ except Exception: # noqa: BLE001 - 落不了盘就当没有,退回「领取时再绑」那条老路
96
+ return None
97
+
98
+
99
+ def agent_id_from_env() -> str:
100
+ from karma_openclaw.agent_env import credential
101
+
102
+ return credential("KARMA_AGENT_ID")
103
+
104
+
105
+ def agent_public_key_b64(key: Any) -> str:
106
+ """Ed25519 public key as base64 of the raw 32 bytes -- the shape the server stores."""
107
+ from cryptography.hazmat.primitives import serialization
108
+
109
+ raw = key.public_key().public_bytes(
110
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
111
+ )
112
+ return base64.b64encode(raw).decode()
113
+
114
+
115
+ def build_agent_request_message(
116
+ *, key_id: str, method: str, path: str, timestamp: str, nonce: str, body_sha256: str
117
+ ) -> str:
118
+ """The exact text the server recomputes. Do not reorder these lines."""
119
+ return "\n".join(
120
+ [
121
+ "Karma Runtime Request",
122
+ "key_id:" + key_id,
123
+ "method:" + method.upper(),
124
+ "path:" + path,
125
+ "timestamp:" + timestamp,
126
+ "nonce:" + nonce,
127
+ "body_sha256:" + body_sha256,
128
+ ]
129
+ )
130
+
131
+
132
+ def build_agent_pairing_request_message(
133
+ *, agent_name: str, public_key: str, nonce: str, timestamp: str
134
+ ) -> str:
135
+ """与 services/runtime_wallet.build_agent_pairing_request_message 逐字一致。"""
136
+ return "\n".join(
137
+ [
138
+ "Karma Agent Pairing Request",
139
+ "agent_name:" + agent_name,
140
+ "public_key:" + public_key,
141
+ "nonce:" + nonce,
142
+ "timestamp:" + timestamp,
143
+ ]
144
+ )
145
+
146
+
147
+ def sign_pairing_request(*, key: Any, agent_name: str, public_key: str) -> dict[str, str]:
148
+ """申请接入要带的三件套:证明申请方真的持有这把公钥对应的私钥。"""
149
+ timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
150
+ nonce = uuid.uuid4().hex
151
+ message = build_agent_pairing_request_message(
152
+ agent_name=agent_name, public_key=public_key, nonce=nonce, timestamp=timestamp
153
+ )
154
+ return {
155
+ "signature": base64.b64encode(key.sign(message.encode("utf-8"))).decode(),
156
+ "nonce": nonce,
157
+ "timestamp": timestamp,
158
+ }
159
+
160
+
161
+ def sign_runtime_request(
162
+ *, key: Any, key_id: str, method: str, path: str, body: bytes
163
+ ) -> dict[str, str]:
164
+ """The three headers a bound key has to carry on every request."""
165
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
166
+ nonce = uuid.uuid4().hex
167
+ message = build_agent_request_message(
168
+ key_id=key_id,
169
+ method=method,
170
+ path=path,
171
+ timestamp=timestamp,
172
+ nonce=nonce,
173
+ body_sha256=hashlib.sha256(body or b"").hexdigest(),
174
+ )
175
+ signature = base64.b64encode(key.sign(message.encode("utf-8"))).decode()
176
+ return {
177
+ "X-Karma-Agent-Signature": signature,
178
+ "X-Karma-Runtime-Timestamp": timestamp,
179
+ "X-Karma-Runtime-Nonce": nonce,
180
+ }
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+ """凭据落在哪、以及怎么把它读回来。
3
+
4
+ `karma_connect_claim` 把凭据写到 ``~/.karma/agent.env``(0600)。但在此之前没有任何
5
+ 代码会把它读回 ``os.environ`` —— 也就是说:**已经跑着的 MCP 进程永远看不到刚领到的
6
+ 凭据**,非要宿主把 MCP server 重启一次才生效。而「宿主重启」恰恰是我们不能假设用户
7
+ 做得到的那一步(OpenClaw 的 config 重载链路本身就可能坏着)。
8
+
9
+ 所以路径解析放在这一个模块里,写盘和读盘共用;HTTP 层每次要用凭据时按文件
10
+ mtime+size 判断是否需要重读,文件变了就立刻生效。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from pathlib import Path
16
+
17
+ #: 这两把钥匙在 agent 侧的用途不同:API key 调公共 API,Runtime key 调 /runtime/*。
18
+ CREDENTIAL_KEYS = (
19
+ "KARMA_API_KEY",
20
+ "KARMA_RUNTIME_KEY",
21
+ "KARMA_RUNTIME_URL",
22
+ "KARMA_AGENT_ID",
23
+ "KARMA_AGENT_PRIVATE_KEY",
24
+ )
25
+
26
+
27
+ def agent_env_path() -> Path:
28
+ """凭据文件位置:``KARMA_AGENT_ENV_PATH`` 覆盖,否则 ``~/.karma/agent.env``。"""
29
+ override = os.environ.get("KARMA_AGENT_ENV_PATH", "").strip()
30
+ if override:
31
+ return Path(override).expanduser()
32
+ return Path.home() / ".karma" / "agent.env"
33
+
34
+
35
+ def read_agent_env() -> dict:
36
+ """把文件解析成 dict;文件不存在或读不动时返回空 dict(不抛)。"""
37
+ path = agent_env_path()
38
+ try:
39
+ text = path.read_text(encoding="utf-8")
40
+ except OSError:
41
+ return {}
42
+ values: dict = {}
43
+ for line in text.splitlines():
44
+ line = line.strip()
45
+ if not line or line.startswith("#") or "=" not in line:
46
+ continue
47
+ key, _, value = line.partition("=")
48
+ key = key.strip()
49
+ if key:
50
+ values[key] = value.strip()
51
+ return values
52
+
53
+
54
+ _cache: dict = {"stamp": None, "values": {}}
55
+
56
+
57
+ def _stamp():
58
+ path = agent_env_path()
59
+ try:
60
+ st = path.stat()
61
+ except OSError:
62
+ return (str(path), None, None)
63
+ return (str(path), st.st_mtime_ns, st.st_size)
64
+
65
+
66
+ def load_agent_env(force: bool = False) -> dict:
67
+ """磁盘上的凭据;文件变了就重读,没变就吃缓存(每次工具调用都会走这里)。"""
68
+ stamp = _stamp()
69
+ if force or _cache["stamp"] != stamp:
70
+ _cache["stamp"] = stamp
71
+ _cache["values"] = read_agent_env()
72
+ return _cache["values"]
73
+
74
+
75
+ def invalidate_agent_env() -> None:
76
+ """刚写完文件时调用,避免同一进程里还吃旧缓存。"""
77
+ _cache["stamp"] = None
78
+ load_agent_env(force=True)
79
+
80
+
81
+ def upsert_credential(name: str, value: str) -> Path:
82
+ """把一对键值并进凭据文件(0600),同名行就地替换。
83
+
84
+ agent 自己的 Ed25519 私钥要先落盘再上报:进程崩了、宿主重启了,它还得在。
85
+ """
86
+ path = agent_env_path()
87
+ path.parent.mkdir(parents=True, exist_ok=True)
88
+ try:
89
+ lines = path.read_text(encoding="utf-8").splitlines()
90
+ except OSError:
91
+ lines = []
92
+ prefix = name + "="
93
+ out: list = []
94
+ replaced = False
95
+ for line in lines:
96
+ if line.strip().startswith(prefix):
97
+ if not replaced:
98
+ out.append(prefix + value)
99
+ replaced = True
100
+ continue
101
+ out.append(line)
102
+ if not replaced:
103
+ out.append(prefix + value)
104
+ text = "\n".join(out).rstrip("\n") + "\n"
105
+ tmp = path.with_suffix(path.suffix + ".tmp")
106
+ tmp.write_text(text, encoding="utf-8")
107
+ try:
108
+ os.chmod(tmp, 0o600)
109
+ except OSError:
110
+ pass
111
+ os.replace(tmp, path)
112
+ invalidate_agent_env()
113
+ return path
114
+
115
+
116
+ def credential(name: str) -> str:
117
+ """环境变量优先(宿主显式注入的压过文件),否则回落到凭据文件。"""
118
+ value = (os.environ.get(name) or "").strip()
119
+ if value:
120
+ return value
121
+ return str(load_agent_env().get(name) or "").strip()
@@ -0,0 +1,104 @@
1
+ """KarmaBilateral MCP tools for OpenClaw — lock/bind/settle via BFF API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+ from urllib.parse import quote
8
+
9
+ from karma_openclaw.http_client import api_get, api_post
10
+
11
+
12
+ def register_bilateral_tools(mcp):
13
+ """Register KarmaBilateral tools on the MCP server."""
14
+
15
+ @mcp.tool()
16
+ async def karma_bilateral_lock(token: str, amount: str, role: str = "buyer") -> dict[str, Any]:
17
+ """Lock USDC into KarmaBilateral and mint a Bill Token.
18
+
19
+ Both buyer AND agent must call this before any task begins.
20
+ This replaces the old lockFunds flow.
21
+
22
+ Args:
23
+ token: ERC-20 token address (use USDC address for testnet)
24
+ amount: Amount in base units, e.g. '10000000' for 10 USDC
25
+ role: 'buyer' or 'agent'
26
+ """
27
+ return await api_post("/v1/bilateral/lock", {
28
+ "token": token,
29
+ "amount": int(amount),
30
+ "role": role,
31
+ })
32
+
33
+ @mcp.tool()
34
+ async def karma_bilateral_bind(buyer_bill_id: int, agent_bill_id: int, scope_description: str) -> dict[str, Any]:
35
+ """Bilaterally bind buyer + agent Bill Tokens. Both sides freeze.
36
+
37
+ Must be called AFTER both parties have locked (karma_bilateral_lock).
38
+ This enters the BOUND state — bills cannot be withdrawn until settled.
39
+
40
+ Args:
41
+ buyer_bill_id: The bill ID from buyer's lock call
42
+ agent_bill_id: The bill ID from agent's lock call
43
+ scope_description: Human-readable task scope, e.g. 'search:latest-pricing'
44
+ """
45
+ import hashlib
46
+ scope_hash = "0x" + hashlib.sha256(scope_description.encode()).hexdigest()
47
+ return await api_post("/v1/bilateral/bind", {
48
+ "buyer_bill_id": buyer_bill_id,
49
+ "agent_bill_id": agent_bill_id,
50
+ "scope_hash": scope_hash,
51
+ })
52
+
53
+ @mcp.tool()
54
+ async def karma_bilateral_settle(binding_id: int, proof_description: str) -> dict[str, Any]:
55
+ """Submit settlement proof for a binding. Enters FINALIZING state.
56
+
57
+ After the settle delay and dispute window pass, call
58
+ karma_bilateral_finalize to complete settlement.
59
+
60
+ Args:
61
+ binding_id: The binding ID from the bind call
62
+ proof_description: What was delivered, e.g. 'ipfs://Qm...evidence'
63
+ """
64
+ import hashlib
65
+ proof_hash = "0x" + hashlib.sha256(proof_description.encode()).hexdigest()
66
+ return await api_post("/v1/bilateral/settle", {
67
+ "binding_id": binding_id,
68
+ "proof_hash": proof_hash,
69
+ })
70
+
71
+ @mcp.tool()
72
+ async def karma_bilateral_finalize(binding_id: int) -> dict[str, Any]:
73
+ """Finalize settlement after dispute window closes. Burns bills, releases USDC.
74
+
75
+ Can only be called after the dispute window has passed since settle().
76
+ """
77
+ return await api_post(f"/v1/bilateral/finalize/{binding_id}", {})
78
+
79
+ @mcp.tool()
80
+ async def karma_bilateral_status(binding_id: int) -> dict[str, Any]:
81
+ """Query the full status of a binding — state, bill states, USDC locked.
82
+
83
+ Returns:
84
+ binding_id, state, bill_states (list), can_settle, can_dispute,
85
+ can_finalize, usdc_locked, settle_after
86
+ """
87
+ return await api_get(f"/v1/bilateral/status/{binding_id}")
88
+
89
+ @mcp.tool()
90
+ async def karma_bilateral_dispute(binding_id: int, reason: str) -> dict[str, Any]:
91
+ """Raise a dispute on a binding in FINALIZING state.
92
+
93
+ Only the buyer can dispute. Must be within the dispute window.
94
+
95
+ Args:
96
+ binding_id: The binding to dispute
97
+ reason: Human-readable reason for dispute
98
+ """
99
+ import hashlib
100
+ evidence_hash = "0x" + hashlib.sha256(reason.encode()).hexdigest()
101
+ # Dispute via direct contract call through BFF
102
+ return await api_post(f"/v1/bilateral/dispute/{binding_id}", {
103
+ "evidence_hash": evidence_hash,
104
+ })