Files
zeekayandHanzo Dev 6b380d62a4 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers: any, max and range over int. omitzero is not applied, since it
changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:15:04 -07:00

928 lines
32 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package governance
import (
"context"
"fmt"
"math/big"
"github.com/luxfi/geth/common"
"github.com/luxfi/mcp"
"github.com/luxfi/mcp/evmread"
)
// The eight read tools — the entire v1 surface. Each reads chain facts via a per-request
// bounded evmread.Caller (eth_call / header reads) and returns a JSON-serializable value
// PLUS a ChainObservation built from the exact reads it performed. NONE writes; there is
// no tx-submitting path here.
const (
toolChainState = "chain_state"
toolParamValue = "param_value"
toolParamHistory = "param_history"
toolThoughtStatus = "thought_status"
toolReceiptLookup = "receipt_lookup"
toolQuorumStatus = "quorum_status"
toolOperatorReputation = "operator_reputation"
toolPendingOperations = "pending_operations"
)
// Thought lifecycle status values (IAIGovernor.Status / AIParams.Status).
const (
statusNone uint8 = 0
statusOpen uint8 = 1
statusSettled uint8 = 2
statusFailed uint8 = 3
)
// Vote values (IAIGovernor.Vote): invalid=0, yes=1, no=2, abstain=3, delay=4, unsafe=5.
// A settling group whose winning vote is anything other than Yes is NOT an approval —
// quorum_status surfaces the winning vote so an operator-LLM never reads a No/Unsafe/Delay
// quorum as a go-ahead.
const (
voteInvalid uint8 = 0
voteYes uint8 = 1
voteNo uint8 = 2
voteAbstain uint8 = 3
voteDelay uint8 = 4
voteUnsafe uint8 = 5
)
// voteLabel maps a Vote value to its IAIGovernor.Vote name (so a winning bucket reads as
// "Yes"/"No"/… and a reader cannot mistake a non-Yes quorum for an approval).
func voteLabel(v uint8) string {
switch v {
case voteYes:
return "Yes"
case voteNo:
return "No"
case voteAbstain:
return "Abstain"
case voteDelay:
return "Delay"
case voteUnsafe:
return "Unsafe"
default:
return "Invalid"
}
}
const defaultLimit = 16
// pendingScanFloor is the minimum number of tail tasks pending_operations inspects even
// when the caller's limit is tiny, so a just-opened task isn't missed behind a few
// freshly-settled ones. The per-request eth_call ceiling (see server.go) is the hard
// backstop against amplification; this only sizes the look-back window.
const pendingScanFloor = int64(64)
// Tools yields the eight read tools as mcp.Tool values. Each Read closure wraps a FRESH
// per-request bounded caller, runs the tool body, and stamps a ChainObservation from the
// facts that body returned — so every tool result is independently verifiable. The
// transport indexes these by Name; this is governance's entire contribution.
func (g *Surface) Tools() []mcp.Tool {
// run executes a body against a fresh per-request bounded caller and builds the
// observation from the facts it returned (closing MED-8: every tool returns a
// ChainObservation of its exact reads). The ceiling lives in the bounded caller.
run := func(name string, body func(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error)) func(context.Context, map[string]any) (any, *mcp.ChainObservation, error) {
return func(ctx context.Context, args map[string]any) (any, *mcp.ChainObservation, error) {
ec := g.bounded()
val, reads, err := body(ctx, ec, args)
if err != nil {
return nil, nil, err
}
obs, err := mcp.NewObservation(ctx, ec, name, reads)
if err != nil {
return nil, nil, err
}
return val, obs, nil
}
}
return []mcp.Tool{
{
Name: toolChainState,
Description: "Current chain head: block number, chain id, latest block hash and timestamp.",
InputSchema: objSchema(nil, nil),
Read: run(toolChainState, g.toolChainState),
},
{
Name: toolParamValue,
Description: "Read a decided governance knob value from AIParams.valueOf(modelSpecHash, knobKey). Returns {value, decided}.",
InputSchema: objSchema(map[string]any{
"modelSpecHash": strSchema("bytes32 model spec hash, 0x-hex"),
"knobKey": strSchema("knob key string"),
}, []string{"modelSpecHash", "knobKey"}),
Read: run(toolParamValue, g.toolParamValue),
},
{
Name: toolParamHistory,
Description: "AIParams round history (newest first): each round with its proposals. Optional limit (default 16) and fromRound.",
InputSchema: objSchema(map[string]any{
"limit": intSchema("max rounds to return (default 16)"),
"fromRound": intSchema("highest round id to start from (default roundCount-1)"),
}, nil),
Read: run(toolParamHistory, g.toolParamHistory),
},
{
Name: toolThoughtStatus,
Description: "AIGovernor.getThought(taskId) fields plus a derived status (Open/Settled/NoQuorum) and taskCount.",
InputSchema: objSchema(map[string]any{
"taskId": intSchema("task id"),
}, []string{"taskId"}),
Read: run(toolThoughtStatus, g.toolThoughtStatus),
},
{
Name: toolReceiptLookup,
Description: "AIThoughtRegistry receipt lookup by receiptId. Returns {exists, receipt, receiptCount}.",
InputSchema: objSchema(map[string]any{
"receiptId": strSchema("bytes32 receipt id, 0x-hex"),
}, []string{"receiptId"}),
Read: run(toolReceiptLookup, g.toolReceiptLookup),
},
{
Name: toolQuorumStatus,
Description: "Quorum tally for a task, computed to match AIGovernor.settle() exactly. " +
"Counts ONLY verdicts from operators still bonded at the observed block (a " +
"withdrawn operator's verdict is dropped, as settle drops it), groups by " +
"(vote,bucket), and reports quorumReached, the winningVote/winningBucket, " +
"winningIsApprove (winning vote == Yes), deadlinePassed and settleable " +
"(quorum reached AND deadline passed — settle reverts before the deadline). " +
"Also returns verdictsTotal/verdictsCounted/droppedUnbonded and observedBlock.",
InputSchema: objSchema(map[string]any{
"taskId": intSchema("task id"),
}, []string{"taskId"}),
Read: run(toolQuorumStatus, g.toolQuorumStatus),
},
{
Name: toolOperatorReputation,
Description: "Operator standing: {isOperator, bond, weight, agreementRateBps, rep} from AIGovernor and AIReputation.",
InputSchema: objSchema(map[string]any{
"operator": strSchema("operator address, 0x-hex"),
}, []string{"operator"}),
Read: run(toolOperatorReputation, g.toolOperatorReputation),
},
{
Name: toolPendingOperations,
Description: "Currently-OPEN (unsettled) thoughts. Optional limit (default 16). Scans only the " +
"TAIL of taskCount (newest tasks), so a still-Open task buried below the window is " +
"NOT returned; when that happens truncated=true and scannedFrom marks the lowest id " +
"inspected (range scannedFrom..taskCount-1). Each entry carries deadlinePassed " +
"(now >= deadline) so a still-Open task whose voting window has closed is visible as " +
"settle-ready. Also returns observedBlock.",
InputSchema: objSchema(map[string]any{
"limit": intSchema("max open thoughts to return (default 16)"),
}, nil),
Read: run(toolPendingOperations, g.toolPendingOperations),
},
}
}
// ----------------------------------------------------------------------------
// 1. chain_state
// ----------------------------------------------------------------------------
func (g *Surface) toolChainState(ctx context.Context, ec evmread.Caller, _ map[string]any) (any, []mcp.ObservedFact, error) {
chainID, err := ec.ChainID(ctx)
if err != nil {
return nil, nil, fmt.Errorf("chain_state: chainID: %w", err)
}
bn, err := ec.BlockNumber(ctx)
if err != nil {
return nil, nil, fmt.Errorf("chain_state: blockNumber: %w", err)
}
hdr, err := ec.HeaderByNumber(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("chain_state: header: %w", err)
}
val := map[string]any{
"chainId": chainID.String(),
"blockNumber": bn,
"blockHash": hdr.Hash().Hex(),
"timestamp": hdr.Time,
}
reads := []mcp.ObservedFact{
{Key: "chainId", Value: chainID.String()},
{Key: "blockNumber", Value: fmt.Sprintf("%d", bn)},
{Key: "blockHash", Value: hdr.Hash().Hex()},
{Key: "timestamp", Value: fmt.Sprintf("%d", hdr.Time)},
}
return val, reads, nil
}
// ----------------------------------------------------------------------------
// 2. param_value
// ----------------------------------------------------------------------------
func (g *Surface) toolParamValue(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
spec, err := argBytes32(args, "modelSpecHash")
if err != nil {
return nil, nil, err
}
key, err := argString(args, "knobKey")
if err != nil {
return nil, nil, err
}
value, decided, err := g.readParamValue(ctx, ec, spec, key)
if err != nil {
return nil, nil, err
}
val := map[string]any{
"value": value.String(),
"decided": decided,
}
reads := []mcp.ObservedFact{
{Key: "knobKey", Value: key},
{Key: "value", Value: value.String()},
{Key: "decided", Value: boolStr(decided)},
}
return val, reads, nil
}
// readParamValue is the shared AIParams.valueOf read (used by the tool and tests).
func (g *Surface) readParamValue(ctx context.Context, ec evmread.Caller, spec [32]byte, key string) (*big.Int, bool, error) {
out, err := g.params.Call(ctx, ec, "valueOf", spec, key)
if err != nil {
return nil, false, err
}
if len(out) != 2 {
return nil, false, fmt.Errorf("param_value: valueOf returned %d values", len(out))
}
value, ok := out[0].(*big.Int)
if !ok {
return nil, false, fmt.Errorf("param_value: value not *big.Int")
}
decided, ok := out[1].(bool)
if !ok {
return nil, false, fmt.Errorf("param_value: decided not bool")
}
return value, decided, nil
}
// ----------------------------------------------------------------------------
// 3. param_history
// ----------------------------------------------------------------------------
func (g *Surface) toolParamHistory(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
limit := argLimit(args, defaultLimit)
count, err := g.readRoundCount(ctx, ec)
if err != nil {
return nil, nil, err
}
rounds, err := g.readParamHistory(ctx, ec, count, argFromRound(args, count), limit)
if err != nil {
return nil, nil, err
}
val := map[string]any{
"roundCount": count.String(),
"rounds": rounds,
}
reads := []mcp.ObservedFact{
{Key: "roundCount", Value: count.String()},
{Key: "roundsReturned", Value: fmt.Sprintf("%d", len(rounds))},
}
return val, reads, nil
}
func (g *Surface) readRoundCount(ctx context.Context, ec evmread.Caller) (*big.Int, error) {
out, err := g.params.Call(ctx, ec, "roundCount")
if err != nil {
return nil, err
}
c, ok := out[0].(*big.Int)
if !ok {
return nil, fmt.Errorf("param_history: roundCount not *big.Int")
}
return c, nil
}
// readParamHistory walks rounds DESCENDING from `from` (inclusive), capped at limit,
// returning each round's fields and its proposals. `from` defaults to count-1 when not
// supplied by the caller; rounds beyond count-1 are clamped.
func (g *Surface) readParamHistory(ctx context.Context, ec evmread.Caller, count, from *big.Int, limit int) ([]any, error) {
out := []any{}
if count.Sign() == 0 {
return out, nil
}
last := new(big.Int).Sub(count, big.NewInt(1))
start := from
if start == nil || start.Cmp(last) > 0 {
start = last
}
for i := new(big.Int).Set(start); i.Sign() >= 0 && len(out) < limit; i.Sub(i, big.NewInt(1)) {
round, err := g.readRound(ctx, ec, i)
if err != nil {
return nil, err
}
proposals, err := g.readProposals(ctx, ec, i)
if err != nil {
return nil, err
}
out = append(out, map[string]any{
"roundId": new(big.Int).Set(i).String(),
"round": roundJSON(round),
"proposals": proposalsJSON(proposals),
})
}
return out, nil
}
func (g *Surface) readRound(ctx context.Context, ec evmread.Caller, roundID *big.Int) (*Round, error) {
r, err := evmread.CallStruct[Round](ctx, g.params, ec, "getRound", roundID)
if err != nil {
return nil, err
}
return &r, nil
}
func (g *Surface) readProposals(ctx context.Context, ec evmread.Caller, roundID *big.Int) ([]Proposal, error) {
return evmread.CallStruct[[]Proposal](ctx, g.params, ec, "getProposals", roundID)
}
// ----------------------------------------------------------------------------
// 4. thought_status
// ----------------------------------------------------------------------------
func (g *Surface) toolThoughtStatus(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
taskID, err := argUint256(args, "taskId")
if err != nil {
return nil, nil, err
}
t, err := g.readThoughtAt(ctx, ec, nil, taskID)
if err != nil {
return nil, nil, err
}
count, err := g.readTaskCountAt(ctx, ec, nil)
if err != nil {
return nil, nil, err
}
res := thoughtJSON(t)
res["status"] = derivedStatus(t.Status)
res["taskCount"] = count.String()
reads := []mcp.ObservedFact{
{Key: "taskId", Value: taskID.String()},
{Key: "status", Value: derivedStatus(t.Status)},
{Key: "canonicalVote", Value: fmt.Sprintf("%d", t.CanonicalVote)},
{Key: "taskCount", Value: count.String()},
}
return res, reads, nil
}
// derivedStatus maps the on-chain Status to the operator-facing label. The chain's
// settle() moves a task Open->Settled on quorum or Open->Failed on no-quorum, so the
// derived label is: Open while accepting verdicts, Settled on quorum, NoQuorum on a Failed
// settle (the task ran but no group reached threshold). None = nonexistent.
func derivedStatus(status uint8) string {
switch status {
case statusOpen:
return "Open"
case statusSettled:
return "Settled"
case statusFailed:
return "NoQuorum"
default:
return "None"
}
}
// ----------------------------------------------------------------------------
// 5. receipt_lookup
// ----------------------------------------------------------------------------
func (g *Surface) toolReceiptLookup(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
id, err := argBytes32(args, "receiptId")
if err != nil {
return nil, nil, err
}
existsOut, err := g.registry.Call(ctx, ec, "exists", id)
if err != nil {
return nil, nil, err
}
exists, ok := existsOut[0].(bool)
if !ok {
return nil, nil, fmt.Errorf("receipt_lookup: exists not bool")
}
countOut, err := g.registry.Call(ctx, ec, "receiptCount")
if err != nil {
return nil, nil, err
}
count, ok := countOut[0].(*big.Int)
if !ok {
return nil, nil, fmt.Errorf("receipt_lookup: receiptCount not *big.Int")
}
res := map[string]any{
"exists": exists,
"receiptCount": count.String(),
}
if exists {
rc, err := evmread.CallStruct[ThoughtReceipt](ctx, g.registry, ec, "getReceipt", id)
if err != nil {
return nil, nil, err
}
res["receipt"] = receiptJSON(&rc)
}
reads := []mcp.ObservedFact{
{Key: "receiptId", Value: common.BytesToHash(id[:]).Hex()},
{Key: "exists", Value: boolStr(exists)},
{Key: "receiptCount", Value: count.String()},
}
return res, reads, nil
}
// ----------------------------------------------------------------------------
// 6. quorum_status
// ----------------------------------------------------------------------------
func (g *Surface) toolQuorumStatus(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
taskID, err := argUint256(args, "taskId")
if err != nil {
return nil, nil, err
}
// Pin every read of this tally to ONE block. settle() decides quorum from the
// operators bonded AT settle time; if we read the verdicts at block N but the bonds at
// a later head, an operator that withdrew in between could be counted (or not)
// inconsistently. Reading thought, verdicts and bonds all at `block` gives a single
// settle-equivalent snapshot and closes that withdraw race.
block, err := g.observedBlock(ctx, ec)
if err != nil {
return nil, nil, err
}
now, err := g.blockTimestamp(ctx, ec, block)
if err != nil {
return nil, nil, err
}
t, err := g.readThoughtAt(ctx, ec, block, taskID)
if err != nil {
return nil, nil, err
}
verdicts, err := g.readVerdictsAt(ctx, ec, block, taskID)
if err != nil {
return nil, nil, err
}
minBond, err := g.readMinBond(ctx, ec, block)
if err != nil {
return nil, nil, err
}
bonded, err := g.readBondedSet(ctx, ec, block, verdicts, minBond)
if err != nil {
return nil, nil, err
}
tally := tallyQuorum(verdicts, t.Threshold, bonded)
// settle()'s liveness gate is `block.timestamp < deadline -> revert` (AIGovernor.sol
// settle, the only gate — there is no full-committee early exit in the code path). So a
// quorum is only ACTUALLY settleable once the deadline has passed.
deadlinePassed := now >= t.Deadline
val := map[string]any{
"verdicts": verdictsJSON(verdicts),
"threshold": t.Threshold,
"deadline": t.Deadline,
"votesFor": tally.votesFor,
"votesAgainst": tally.votesAgainst,
"quorumReached": tally.quorumReached,
"bestGroup": tally.bestCount,
"winningVote": tally.winningVote,
"winningVoteLabel": voteLabel(tally.winningVote),
"winningBucket": tally.winningBucket,
"winningIsApprove": tally.quorumReached && tally.winningVote == voteYes,
"deadlinePassed": deadlinePassed,
// settleable mirrors what settle() will accept right now: a reached quorum whose
// deadline has passed. Mid-window a reached quorum is NOT yet settleable.
"settleable": tally.quorumReached && deadlinePassed,
"verdictsTotal": len(verdicts),
"verdictsCounted": tally.counted,
"droppedUnbonded": len(verdicts) - tally.counted,
"observedBlock": block.String(),
}
reads := []mcp.ObservedFact{
{Key: "taskId", Value: taskID.String()},
{Key: "observedBlock", Value: block.String()},
{Key: "quorumReached", Value: boolStr(tally.quorumReached)},
{Key: "winningVote", Value: fmt.Sprintf("%d", tally.winningVote)},
{Key: "verdictsCounted", Value: fmt.Sprintf("%d", tally.counted)},
{Key: "verdictsTotal", Value: fmt.Sprintf("%d", len(verdicts))},
{Key: "settleable", Value: boolStr(tally.quorumReached && deadlinePassed)},
}
return val, reads, nil
}
func (g *Surface) readVerdictsAt(ctx context.Context, ec evmread.Caller, block, taskID *big.Int) ([]Verdict, error) {
return evmread.CallStructAt[[]Verdict](ctx, g.governor, ec, block, "getVerdicts", taskID)
}
func (g *Surface) readThoughtAt(ctx context.Context, ec evmread.Caller, block, taskID *big.Int) (*Thought, error) {
t, err := evmread.CallStructAt[Thought](ctx, g.governor, ec, block, "getThought", taskID)
if err != nil {
return nil, err
}
return &t, nil
}
// readMinBond reads AIGovernor.minBond() at `block` — the bonded-eligibility floor
// settle() applies (see _bonded).
func (g *Surface) readMinBond(ctx context.Context, ec evmread.Caller, block *big.Int) (*big.Int, error) {
out, err := g.governor.CallAt(ctx, ec, block, "minBond")
if err != nil {
return nil, err
}
mb, ok := out[0].(*big.Int)
if !ok {
return nil, fmt.Errorf("quorum_status: minBond not *big.Int")
}
return mb, nil
}
// readBondedSet returns the set of verdict operators that are BONDED at `block` under the
// contract's exact settle predicate: AIGovernor._bonded(who) == (bondOf(who) != 0 &&
// bondOf(who) >= minBond). It deliberately does NOT consider the deregister flag — a
// deregistered-but-still-bonded operator's verdict still counts at settle, and only a
// fully-withdrawn (bond == 0) operator is dropped. Each distinct operator is read once.
func (g *Surface) readBondedSet(ctx context.Context, ec evmread.Caller, block *big.Int, verdicts []Verdict, minBond *big.Int) (map[common.Address]bool, error) {
bonded := make(map[common.Address]bool, len(verdicts))
for i := range verdicts {
op := verdicts[i].Operator
if _, seen := bonded[op]; seen {
continue
}
out, err := g.governor.CallAt(ctx, ec, block, "bondOf", op)
if err != nil {
return nil, err
}
bond, ok := out[0].(*big.Int)
if !ok {
return nil, fmt.Errorf("quorum_status: bondOf not *big.Int")
}
// _bonded: bond != 0 && bond >= minBond.
bonded[op] = bond.Sign() != 0 && bond.Cmp(minBond) >= 0
}
return bonded, nil
}
// observedBlock pins the block this tool's reads are taken at: the current head number.
// Reading it once and threading it through every call gives a consistent snapshot.
func (g *Surface) observedBlock(ctx context.Context, ec evmread.Caller) (*big.Int, error) {
bn, err := ec.BlockNumber(ctx)
if err != nil {
return nil, fmt.Errorf("mcp: observed block number: %w", err)
}
return new(big.Int).SetUint64(bn), nil
}
// blockTimestamp returns the timestamp of `block` (the same point the reads reflect), used
// to evaluate settle()'s deadline gate against the observed state, not a later head.
func (g *Surface) blockTimestamp(ctx context.Context, ec evmread.Caller, block *big.Int) (uint64, error) {
hdr, err := ec.HeaderByNumber(ctx, block)
if err != nil {
return 0, fmt.Errorf("mcp: observed block header: %w", err)
}
return hdr.Time, nil
}
type quorumTally struct {
votesFor int // Yes verdicts among BONDED operators (what settle would tally)
votesAgainst int // No verdicts among BONDED operators
bestCount int
counted int // total BONDED verdicts considered
winningVote uint8
winningBucket uint16
quorumReached bool
}
// tallyQuorum reproduces AIGovernor.settle()'s tally EXACTLY: it considers ONLY verdicts
// whose operator is bonded at the observed block (bonded[op] true), groups them by the
// consensus key (vote, confidenceBucket) — the same _consensusKey settle uses — tracks the
// largest group, and reports quorumReached = (largest group >= threshold). A verdict from a
// withdrawn (unbonded) operator is DROPPED, just as settle drops it, so MCP cannot report
// quorumReached=true for a quorum the chain will settle Failed. winningVote/winningBucket
// name the best group so a non-Yes quorum is legible. votesFor / votesAgainst are the Yes /
// No counts among the BONDED set. Pure projection of on-chain reads — it submits nothing.
//
// Structural invariant (load-bearing): AIGovernor sets threshold = n/2 + 1, so AT MOST ONE
// (vote,bucket) group can ever reach quorum — two groups clearing threshold would need
// 2*threshold <= n, i.e. n+2 <= n, impossible. A withdrawal can therefore only DESTROY a
// quorum, never transfer it to a different vote-group. The first-seen strict-`>` tie-break
// below thus only governs the cosmetic winningVote/winningBucket on the no-quorum path
// (settle records Invalid there). If the threshold formula in AIGovernor.sol ever drops
// below n/2+1, this single-winner guarantee breaks and the tie-break parity with settle()
// becomes quorum-affecting — re-audit HIGH-1 then.
func tallyQuorum(verdicts []Verdict, threshold uint8, bonded map[common.Address]bool) quorumTally {
type key struct {
vote uint8
bucket uint16
}
groups := map[key]int{}
var t quorumTally
// Iterate in verdict order so ties resolve to the FIRST-seen group, matching settle()'s
// submitter-order scan (it keeps the earliest key at a given bestCount).
var bestKey key
haveBest := false
for i := range verdicts {
v := verdicts[i]
if !bonded[v.Operator] {
continue
}
t.counted++
k := key{v.Vote, v.ConfidenceBucket}
groups[k]++
switch v.Vote {
case voteYes:
t.votesFor++
case voteNo:
t.votesAgainst++
}
if c := groups[k]; c > t.bestCount {
t.bestCount = c
bestKey = k
haveBest = true
}
}
if haveBest {
t.winningVote = bestKey.vote
t.winningBucket = bestKey.bucket
}
t.quorumReached = t.bestCount >= int(threshold) && threshold > 0
return t
}
// ----------------------------------------------------------------------------
// 7. operator_reputation
// ----------------------------------------------------------------------------
func (g *Surface) toolOperatorReputation(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
op, err := argAddress(args, "operator")
if err != nil {
return nil, nil, err
}
isOpOut, err := g.governor.Call(ctx, ec, "isOperator", op)
if err != nil {
return nil, nil, err
}
isOp, ok := isOpOut[0].(bool)
if !ok {
return nil, nil, fmt.Errorf("operator_reputation: isOperator not bool")
}
bondOut, err := g.governor.Call(ctx, ec, "bondOf", op)
if err != nil {
return nil, nil, err
}
bond, ok := bondOut[0].(*big.Int)
if !ok {
return nil, nil, fmt.Errorf("operator_reputation: bondOf not *big.Int")
}
weightOut, err := g.rep.Call(ctx, ec, "weightOf", op)
if err != nil {
return nil, nil, err
}
weight, ok := weightOut[0].(uint32)
if !ok {
return nil, nil, fmt.Errorf("operator_reputation: weightOf not uint32")
}
rateOut, err := g.rep.Call(ctx, ec, "agreementRateBps", op)
if err != nil {
return nil, nil, err
}
rate, ok := rateOut[0].(uint32)
if !ok {
return nil, nil, fmt.Errorf("operator_reputation: agreementRateBps not uint32")
}
rep, err := evmread.CallStruct[Rep](ctx, g.rep, ec, "repOf", op)
if err != nil {
return nil, nil, err
}
val := map[string]any{
"isOperator": isOp,
"bond": bond.String(),
"weight": weight,
"agreementRateBps": rate,
"rep": repJSON(&rep),
}
reads := []mcp.ObservedFact{
{Key: "operator", Value: op.Hex()},
{Key: "isOperator", Value: boolStr(isOp)},
{Key: "bond", Value: bond.String()},
{Key: "weight", Value: fmt.Sprintf("%d", weight)},
{Key: "agreementRateBps", Value: fmt.Sprintf("%d", rate)},
}
return val, reads, nil
}
// ----------------------------------------------------------------------------
// 8. pending_operations
// ----------------------------------------------------------------------------
func (g *Surface) toolPendingOperations(ctx context.Context, ec evmread.Caller, args map[string]any) (any, []mcp.ObservedFact, error) {
limit := argLimit(args, defaultLimit)
// Pin reads to one block so taskCount, each thought, and the "now" used for the
// deadlinePassed flag all reflect the same chain point.
block, err := g.observedBlock(ctx, ec)
if err != nil {
return nil, nil, err
}
now, err := g.blockTimestamp(ctx, ec, block)
if err != nil {
return nil, nil, err
}
count, err := g.readTaskCountAt(ctx, ec, block)
if err != nil {
return nil, nil, err
}
open, truncated, scannedFrom, err := g.readPendingOperations(ctx, ec, block, count, limit, now)
if err != nil {
return nil, nil, err
}
val := map[string]any{
"taskCount": count.String(),
"pending": open,
// truncated is true when OLDER tasks below the scanned window were NOT inspected, so
// the caller knows this list may be incomplete (a deep still-Open task can hide below
// the tail window). scannedFrom..(taskCount-1) is the range looked at.
"truncated": truncated,
"scannedFrom": scannedFrom.String(),
"observedBlock": block.String(),
}
reads := []mcp.ObservedFact{
{Key: "observedBlock", Value: block.String()},
{Key: "taskCount", Value: count.String()},
{Key: "pendingReturned", Value: fmt.Sprintf("%d", len(open))},
{Key: "truncated", Value: boolStr(truncated)},
{Key: "scannedFrom", Value: scannedFrom.String()},
}
return val, reads, nil
}
// readPendingOperations scans the tail of taskCount descending and returns thoughts still
// in Open status, capped at limit. (Open means the task is accepting verdicts or awaiting
// settle; the chain only leaves Open via settle.) Each entry is annotated with
// deadlinePassed = now >= deadline, so the caller sees that a still-"Open" task whose voting
// window has CLOSED is settle-ready even though settle was never called. Returns
// truncated=true when the scan stopped above task 0 (older tasks were not inspected), and
// the lowest task id scanned. The bounded window plus the per-request eth_call ceiling keep
// a huge taskCount from issuing an unbounded scan.
func (g *Surface) readPendingOperations(ctx context.Context, ec evmread.Caller, block, count *big.Int, limit int, now uint64) ([]any, bool, *big.Int, error) {
out := []any{}
last := new(big.Int).Sub(count, big.NewInt(1))
if count.Sign() == 0 {
return out, false, big.NewInt(0), nil
}
scanWindow := max(int64(limit), pendingScanFloor)
floor := new(big.Int).Sub(last, big.NewInt(scanWindow-1))
if floor.Sign() < 0 {
floor = big.NewInt(0)
}
// lowestInspected tracks the smallest task id we actually read; truncated is then simply
// "did we stop before reaching task 0". This is honest whether the loop ended on the
// limit or on the window floor.
lowestInspected := new(big.Int).Set(last)
for i := new(big.Int).Set(last); i.Cmp(floor) >= 0 && len(out) < limit; i.Sub(i, big.NewInt(1)) {
lowestInspected.Set(i)
t, err := g.readThoughtAt(ctx, ec, block, i)
if err != nil {
return nil, false, nil, err
}
if t.Status != statusOpen {
continue
}
res := thoughtJSON(t)
res["taskId"] = new(big.Int).Set(i).String()
res["status"] = derivedStatus(t.Status)
// A still-Open task past its deadline is settle-ready but un-settled — flag it so the
// LLM does not read "Open" as "still accepting / outcome not yet fixed".
res["deadlinePassed"] = now >= t.Deadline
out = append(out, res)
}
truncated := lowestInspected.Sign() > 0
return out, truncated, lowestInspected, nil
}
func (g *Surface) readTaskCountAt(ctx context.Context, ec evmread.Caller, block *big.Int) (*big.Int, error) {
out, err := g.governor.CallAt(ctx, ec, block, "taskCount")
if err != nil {
return nil, err
}
c, ok := out[0].(*big.Int)
if !ok {
return nil, fmt.Errorf("pending_operations: taskCount not *big.Int")
}
return c, nil
}
// ----------------------------------------------------------------------------
// JSON projections — stable, all-scalar renderings of the mirror structs.
// ----------------------------------------------------------------------------
func roundJSON(r *Round) map[string]any {
return map[string]any{
"modelSpecHash": hexBytes32(r.ModelSpecHash),
"promptHash": hexBytes32(r.PromptHash),
"knobKey": r.KnobKey,
"lo": bigString(r.Lo),
"hi": bigString(r.Hi),
"n": r.N,
"threshold": r.Threshold,
"openedAt": r.OpenedAt,
"deadline": r.Deadline,
"opener": r.Opener.Hex(),
"status": r.Status,
"statusLabel": derivedStatus(r.Status),
"submissionCount": r.SubmissionCount,
"canonicalValue": bigString(r.CanonicalValue),
}
}
func proposalsJSON(ps []Proposal) []any {
out := make([]any, 0, len(ps))
for i := range ps {
out = append(out, map[string]any{
"operator": ps[i].Operator.Hex(),
"value": bigString(ps[i].Value),
"confidenceBucket": ps[i].ConfidenceBucket,
"evidenceHash": hexBytes32(ps[i].EvidenceHash),
"submittedAt": ps[i].SubmittedAt,
})
}
return out
}
func thoughtJSON(t *Thought) map[string]any {
return map[string]any{
"modelSpecHash": hexBytes32(t.ModelSpecHash),
"promptHash": hexBytes32(t.PromptHash),
"evidenceHash": hexBytes32(t.EvidenceHash),
"n": t.N,
"threshold": t.Threshold,
"openedAt": t.OpenedAt,
"deadline": t.Deadline,
"opener": t.Opener.Hex(),
"rawStatus": t.Status,
"submissionCount": t.SubmissionCount,
"knobKey": t.KnobKey,
"canonicalVote": t.CanonicalVote,
"canonicalBucket": t.CanonicalBucket,
"agreeCount": t.AgreeCount,
"evidenceRoot": hexBytes32(t.EvidenceRoot),
"commitReveal": t.CommitReveal,
"commitDeadline": t.CommitDeadline,
"revealDeadline": t.RevealDeadline,
}
}
func verdictsJSON(vs []Verdict) []any {
out := make([]any, 0, len(vs))
for i := range vs {
out = append(out, map[string]any{
"operator": vs[i].Operator.Hex(),
"vote": vs[i].Vote,
"confidenceBucket": vs[i].ConfidenceBucket,
"evidenceHash": hexBytes32(vs[i].EvidenceHash),
"submittedAt": vs[i].SubmittedAt,
})
}
return out
}
func receiptJSON(rc *ThoughtReceipt) map[string]any {
return map[string]any{
"modelId": hexBytes32(rc.ModelId),
"promptHash": hexBytes32(rc.PromptHash),
"outputHash": hexBytes32(rc.OutputHash),
"paymentHash": hexBytes32(rc.PaymentHash),
"quorumProof": hexBytes32(rc.QuorumProof),
"payer": rc.Payer.Hex(),
"operator": rc.Operator.Hex(),
"cost": bigString(rc.Cost),
"registeredAt": rc.RegisteredAt,
"blockNumber": rc.BlockNumber,
}
}
func repJSON(r *Rep) map[string]any {
return map[string]any{
"weightBps": r.WeightBps,
"participated": r.Participated,
"agreed": r.Agreed,
"lastTaskId1": r.LastTaskId1,
"lastUpdated": r.LastUpdated,
}
}
func hexBytes32(b [32]byte) string {
return common.BytesToHash(b[:]).Hex()
}
// bigString renders a *big.Int as a decimal string, treating nil as "0".
func bigString(v *big.Int) string {
if v == nil {
return "0"
}
return v.String()
}
// boolStr renders a bool as the canonical "true"/"false" string used in observation facts.
func boolStr(b bool) string {
if b {
return "true"
}
return "false"
}