Skip to content

Commit 0126e3a

Browse files
author
Neo
committed
Production hardening: SIWE auth, streaming, persistence, .env, and bridge fixes
Gateway: - /auth/nonce + /auth/verify SIWE endpoints with single-use nonces and TTL-bounded session tokens (X-Wallet-Session) - /chat/stream Server-Sent Events endpoint and /ws WebSocket endpoint, both persisting conversations through MemoryManager - handle_chat now hydrates from disk on first access and writes back via save_conversation after every turn - /status now reports subsystems (models, memory, blockchain, protocols) - .env support via python-dotenv with env-var overrides for keys, ports, RPC URL, and notification tokens Memory: - Per-agent asyncio.Lock for read-modify-write safety - Atomic file writes via tmp + os.replace - Async write/save_turn/save_conversation/mark_first_boot_sent - Dedicated conversations/ subdir and first_boot.json tracker Bridge (gateway/bridge.py): - Removed dead test imports (make_mocked_request, AsyncMock) - Stripped uninstantiable BlockchainInterface() calls in wallet_status and get_dashboard; balance lookups now return None pending a real provider client - link_wallet now requires X-Wallet-Session and takes the verified address from the server-side SIWE session — body addresses are ignored Models: - Restored Mythos provider; model string updated to claude-opus-4-6 SDK: - astream_chat is now a real SSE client against /chat/stream, falling back to /chat on non-200 Docs/config: - contracts/DEPLOYMENT_GUIDE.md and .env.example - openmatrix.config.json.example: mythos block + auth/anon rate limits Tests: - Updated MemoryManager tests for async write - Updated gateway test fixture for new rate limiter, wallet sessions, and async memory mocks - Full suite: 149 passing
1 parent 0ea74f9 commit 0126e3a

14 files changed

Lines changed: 951 additions & 132 deletions

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# 0pnMatrx environment overrides
2+
#
3+
# Copy to `.env` and fill in the values you want to override.
4+
# Anything set here takes precedence over openmatrix.config.json.
5+
6+
# ── Gateway ──
7+
# OPENMATRIX_API_KEY=
8+
# OPENMATRIX_HOST=0.0.0.0
9+
# OPENMATRIX_PORT=18790
10+
11+
# ── Model providers ──
12+
# OPENAI_API_KEY=
13+
# ANTHROPIC_API_KEY=
14+
# NVIDIA_API_KEY=
15+
# GOOGLE_API_KEY=
16+
17+
# ── Blockchain ──
18+
# BASE_RPC_URL=https://sepolia.base.org
19+
20+
# ── Notifications ──
21+
# TELEGRAM_BOT_TOKEN=

contracts/DEPLOYMENT_GUIDE.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 0pnMatrx Contract Deployment Guide
2+
3+
This guide walks you through deploying the 0pnMatrx smart-contract suite to
4+
Base Sepolia (testnet) or Base Mainnet, then plugging the resulting addresses
5+
into `openmatrix.config.json`.
6+
7+
## Prerequisites
8+
9+
- Python 3.10+
10+
- An RPC URL for Base (default Sepolia: `https://sepolia.base.org`)
11+
- A funded deployer wallet
12+
- Sepolia ETH faucet: <https://www.alchemy.com/faucets/base-sepolia>
13+
- `solc` 0.8.20 (auto-installed by `py-solc-x` on first run)
14+
15+
## 1. Configure environment
16+
17+
Copy `.env.example` to `.env` and fill in:
18+
19+
```
20+
BASE_RPC_URL=https://sepolia.base.org
21+
```
22+
23+
Set the deployer key in `openmatrix.config.json`:
24+
25+
```json
26+
"blockchain": {
27+
"network": "base-sepolia",
28+
"rpc_url": "https://sepolia.base.org",
29+
"chain_id": 84532,
30+
"demo_wallet_private_key": "0x...",
31+
"demo_wallet_address": "0x..."
32+
}
33+
```
34+
35+
## 2. Compile and deploy
36+
37+
```bash
38+
python -m contracts.deploy
39+
```
40+
41+
The deployer compiles each `.sol` file under `contracts/`, deploys it, and
42+
writes the resulting addresses to `contracts/deployed_addresses.json`.
43+
44+
## 3. Wire addresses into config
45+
46+
After deployment, copy each address from `deployed_addresses.json` into the
47+
matching `services.*` block in `openmatrix.config.json`. For example:
48+
49+
```json
50+
"services": {
51+
"marketplace": {
52+
"enabled": true,
53+
"contract_address": "0xYourMarketplaceAddress",
54+
"platform_fee_bps": 500,
55+
"platform_wallet": "0xYourPlatformWallet"
56+
}
57+
}
58+
```
59+
60+
## 4. Verify
61+
62+
```bash
63+
curl http://localhost:18790/status | jq .subsystems.blockchain
64+
```
65+
66+
You should see `{"configured": true}`.
67+
68+
## Reference: Pre-deployed addresses
69+
70+
The 0pnMatrx team maintains a public deployment on Base Sepolia for testing.
71+
These addresses are stable and can be used directly in your config:
72+
73+
| Service | Base Sepolia |
74+
|----------------------|----------------------------------------------------|
75+
| EAS (Attestation) | `0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587` |
76+
| Marketplace | _populate after deploy_ |
77+
| Staking | _populate after deploy_ |
78+
| DAO factory | _populate after deploy_ |
79+
| DID registry | _populate after deploy_ |
80+
| Insurance | _populate after deploy_ |
81+
| NFT factory | _populate after deploy_ |
82+
| DEX router | _populate after deploy_ |
83+
| Paymaster | _populate after deploy_ |
84+
85+
> If you deploy your own copies, please open a PR adding your addresses to
86+
> this table so others can reuse them.
87+
88+
## Troubleshooting
89+
90+
- **`insufficient funds`** — top up the deployer wallet at the faucet above.
91+
- **`replacement transaction underpriced`** — increase gas in the deployer
92+
config or wait for the previous tx to confirm.
93+
- **`solc not found`**`py-solc-x` should auto-install on first run; if it
94+
fails, run `python -c "import solcx; solcx.install_solc('0.8.20')"`.

gateway/bridge.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,6 @@ async def chat(self, request: web.Request) -> web.Response:
602602
agent = body.get("agent", "trinity")
603603
session_id = body.get("session_id", "default")
604604

605-
# Delegate to the main gateway handler
606-
# Build a fake request for handle_chat
607-
from aiohttp.test_utils import make_mocked_request
608-
from unittest.mock import AsyncMock
609-
610-
# Just call the chat logic directly
611605
try:
612606
result = await self._handle_chat_internal(message, agent, session_id, body)
613607
return MobileResponse.ok(result)
@@ -709,27 +703,46 @@ async def execute_action(self, request: web.Request) -> web.Response:
709703
# ─── Wallet ───────────────────────────────────────────────────────────
710704

711705
async def link_wallet(self, request: web.Request) -> web.Response:
712-
"""Link a wallet address to a session."""
706+
"""Link a SIWE-verified wallet to a session.
707+
708+
Requires the ``X-Wallet-Session`` header containing a session token
709+
previously issued by ``POST /auth/verify``. The verified address is
710+
taken from the server-side session record — any address supplied in
711+
the request body is ignored to prevent impersonation.
712+
"""
713+
wallet_token = request.headers.get("X-Wallet-Session", "")
714+
if not wallet_token:
715+
return MobileResponse.error(
716+
"X-Wallet-Session header required (sign in via /auth/verify first)",
717+
401,
718+
)
719+
720+
wallet_sessions = getattr(self._server, "wallet_sessions", {})
721+
wallet_session = wallet_sessions.get(wallet_token)
722+
if not wallet_session:
723+
return MobileResponse.error("invalid or expired wallet session", 401)
724+
713725
try:
714726
body = await request.json()
715727
except Exception:
716-
return MobileResponse.error("Invalid JSON")
728+
body = {}
717729

718730
session_id = body.get("session_id", "")
719-
address = body.get("address", "")
720-
721-
if not session_id or not address:
722-
return MobileResponse.error("session_id and address required")
731+
if not session_id:
732+
return MobileResponse.error("session_id required")
723733

734+
address = wallet_session["address"]
724735
self._linked_wallets[session_id] = {
725736
"address": address,
726737
"linked_at": time.time(),
727738
"network": body.get("network", "base-sepolia"),
739+
"verified": True,
728740
}
729741

730742
return MobileResponse.ok({
731743
"linked": True,
732744
"address": address,
745+
"verified": True,
733746
})
734747

735748
async def wallet_status(self, request: web.Request) -> web.Response:
@@ -740,21 +753,15 @@ async def wallet_status(self, request: web.Request) -> web.Response:
740753
if not wallet:
741754
return MobileResponse.ok({"linked": False})
742755

743-
# Try to get balance
744-
balance = None
745-
try:
746-
from runtime.blockchain.interface import BlockchainInterface
747-
blockchain = BlockchainInterface(self._config)
748-
balance_wei = blockchain.w3.eth.get_balance(wallet["address"])
749-
balance = str(blockchain.w3.from_wei(balance_wei, "ether"))
750-
except Exception:
751-
pass
752-
756+
# TODO: Wire balance lookup through a concrete provider client
757+
# (e.g. Web3 / Alchemy). BlockchainInterface is an abstract base
758+
# and cannot be instantiated directly.
753759
return MobileResponse.ok({
754760
"linked": True,
755761
"address": wallet["address"],
756762
"network": wallet.get("network", "base-sepolia"),
757-
"balance_eth": balance,
763+
"balance_eth": None,
764+
"verified": wallet.get("verified", False),
758765
})
759766

760767
# ─── App Config ───────────────────────────────────────────────────────
@@ -886,21 +893,13 @@ async def get_dashboard(self, request: web.Request) -> web.Response:
886893
}
887894

888895
if wallet:
889-
try:
890-
from runtime.blockchain.interface import BlockchainInterface
891-
blockchain = BlockchainInterface(self._config)
892-
balance_wei = blockchain.w3.eth.get_balance(wallet["address"])
893-
balance = str(blockchain.w3.from_wei(balance_wei, "ether"))
894-
dashboard["wallet"] = {
895-
"address": wallet["address"],
896-
"balance_eth": balance,
897-
"network": wallet.get("network"),
898-
}
899-
except Exception:
900-
dashboard["wallet"] = {
901-
"address": wallet["address"],
902-
"balance_eth": None,
903-
"network": wallet.get("network"),
904-
}
896+
# TODO: Wire balance lookup through a concrete provider client.
897+
# BlockchainInterface is an abstract base — see wallet_status().
898+
dashboard["wallet"] = {
899+
"address": wallet["address"],
900+
"balance_eth": None,
901+
"network": wallet.get("network"),
902+
"verified": wallet.get("verified", False),
903+
}
905904

906905
return MobileResponse.ok(dashboard)

0 commit comments

Comments
 (0)