mirror of
https://github.com/luxfi/mcp.git
synced 2026-07-26 23:59:51 +00:00
Split the flat governance MCP package into three independently-complete
packages so the generic parts are written ONCE and future MCP surfaces
(mining, treasury, bridge) are just a bag of read tools, never re-implementing
transport. One and only one way:
github.com/luxfi/mcp (root) - domain-agnostic READ-ONLY MCP
transport (Serve, Tool, Surface,
dispatch, per-call timeout, line
cap, panic->JSON-RPC recover) +
ChainObservation. Imports NO geth.
github.com/luxfi/mcp/evmread - the SOLE go-ethereum importer: a
minimal read-only Caller, Contract,
Call/CallStruct[T], and the
per-request Bounded call ceiling.
github.com/luxfi/mcp/governance - a Surface of the 8 read tools;
reads via evmread, contributes
[]mcp.Tool to the shared transport.
github.com/luxfi/mcp/cmd/lux-mcp - the binary (was aivm-gov-mcp).
What this generalizes (the real point, not just a move):
- Tool is now a value carrying its own Read closure; Serve composes
...Surface and rejects duplicate / empty / nil-Read tools.
- ChainObservation lifted to the root and WIRED INTO ALL 8 TOOLS: every read
returns (value, *ChainObservation) so an operator-LLM's signed verdict can
bind Observation.Hash() and re-verify the chain state it claims to have seen
- domain-independently. (Closes the previously-deferred MED-8.)
- The read-only AST gate is now MODULE-WIDE: it proves go-ethereum is imported
by exactly one package (evmread), no package imports bind/geth-crypto/
geth-rpc/reflect, the string-fragment denylist holds, and evmread.Caller's
method set is exactly {CallContract,ChainID,BlockNumber,HeaderByNumber}.
Behavior-preserving and adversarially re-verified (blue->red loop):
- HIGH-1 bonded-quorum tally still matches AIGovernor.settle()'s _bonded
filter exactly (PoC drives real compiled Solidity via core/vm/runtime).
- Parity tests remain real (independent ABI decode vs MCP output), struct
mirrors still field-for-field aligned with luxfi/standard.
- Per-request bounded ceiling cannot leak/bypass; line cap + timeout + recover
preserved. Read-only invariant intact and STRONGER (adding a write method to
Caller is now a compile failure).
- Tests: 33 pass, 0 fail, 0 skip across the 3 packages (was 23 pre-split;
none dropped, several strengthened); go build / vet / -race clean.
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mcp
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// TestObservationDedupsDuplicateKeys is the NIT test: an observation whose Reads name the
|
|
// SAME key twice must canonicalize deterministically (last-writer-wins), so two parties
|
|
// cannot produce divergent hashes from a dup-key set, and the dup collapses to one entry.
|
|
// White-box (package mcp) because it asserts the unexported sortReads collapse directly.
|
|
func TestObservationDedupsDuplicateKeys(t *testing.T) {
|
|
mk := func(reads []ObservedFact) *ChainObservation {
|
|
return &ChainObservation{
|
|
ChainID: big.NewInt(7),
|
|
BlockNumber: 42,
|
|
BlockHash: common.HexToHash("0x01"),
|
|
Timestamp: 1000,
|
|
Tool: "param_value",
|
|
Reads: reads,
|
|
}
|
|
}
|
|
// Two sets that differ only in the ORDER of a duplicated "value" key. Last writer
|
|
// ("final") must win in BOTH, so the canonical bytes and hash are identical.
|
|
a := mk([]ObservedFact{
|
|
{Key: "knobKey", Value: "temp"},
|
|
{Key: "value", Value: "first"},
|
|
{Key: "value", Value: "final"},
|
|
})
|
|
b := mk([]ObservedFact{
|
|
{Key: "value", Value: "first"},
|
|
{Key: "knobKey", Value: "temp"},
|
|
{Key: "value", Value: "final"},
|
|
})
|
|
if string(a.Canonical()) != string(b.Canonical()) {
|
|
t.Fatalf("dup-key observations not canonical-equal:\n a=%s\n b=%s", a.Canonical(), b.Canonical())
|
|
}
|
|
if a.Hash() != b.Hash() {
|
|
t.Fatalf("dup-key observations hashed differently: %s vs %s", a.Hash().Hex(), b.Hash().Hex())
|
|
}
|
|
|
|
// The duplicate must collapse to a single "value" entry equal to the last writer.
|
|
c := mk([]ObservedFact{
|
|
{Key: "value", Value: "first"},
|
|
{Key: "value", Value: "final"},
|
|
})
|
|
c.sortReads()
|
|
count := 0
|
|
var kept string
|
|
for _, r := range c.Reads {
|
|
if r.Key == "value" {
|
|
count++
|
|
kept = r.Value
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("duplicate key not collapsed: %d 'value' entries remain", count)
|
|
}
|
|
if kept != "final" {
|
|
t.Fatalf("last-writer-wins broken: kept %q, want final", kept)
|
|
}
|
|
}
|