@mcsherrylabs/evolver-mcp 0.8.3 → 0.8.4

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.
@@ -0,0 +1,92 @@
1
+ # devnet genesis — the committee, plus SEED NODES (170).
2
+ #
3
+ # ⚠️ THIS FILE IS THE CHAIN'S IDENTITY. Its canonical hash is the genesis block id, so any edit
4
+ # here produces a DIFFERENT CHAIN. Every node must hold a byte-identical copy; a node with an
5
+ # older one does not disagree politely, it cannot join at all.
6
+ #
7
+ # ── Why the seed entries exist (170, issue #121) ────────────────────────────────────────────
8
+ # A node starting with an empty volume can only learn a peer's ADDRESS from this file, and can
9
+ # only VERIFY a peer whose key is already in its chain state — which, before it has synced, is
10
+ # also only this file. Both roads lead here. Before 170 this file named one node, the committee,
11
+ # whose `allowed-inbound` is deliberately full, so a newcomer could reach nothing it was able to
12
+ # trust and nothing it was able to trust would accept it. No configuration could fix that.
13
+ #
14
+ # A `quorum-weight = 0` entry is present-and-trusted WITHOUT being on the committee: it is seeded
15
+ # as an identity, its Ed25519 key becomes verifiable, and its address becomes dialable — while
16
+ # `seedGenesis` keeps it out of the quorum store and leader rotation filters it out of the
17
+ # proposer set. That is the whole mechanism; no new key, no new trust path.
18
+ #
19
+ # ⚠️ The two front nodes keep the names they already had. They were NOT in genesis before, so a
20
+ # chain rebuild would have reassigned their names and invalidated every pin across the estate and
21
+ # the published installer. Pinning them here makes the names stable by construction.
22
+ ce.evolver.genesis.nodes = [
23
+ # ── the committee ──────────────────────────────────────────────────────────────────────────
24
+ {
25
+ node-id = "devnet1-node0"
26
+ host = "devnet.mcsherrylabs.com"
27
+ port = 7000
28
+ public-key = "MCowBQYDK2VwAyEAxTC3mipBz3Ae6nkUzORWLAco21eX3iSSC+QEUSbJMCs="
29
+ bls-public-key = "rCrpHeLH85ECkow6A58I54fjvtCmOHfdm1llPrish+aD49l32sBciyYAzytRxwak"
30
+ quorum-weight = 1000
31
+ },
32
+
33
+ # ── seed nodes: weight 0, so present and trusted but never voting, never leading ───────────
34
+ #
35
+ # ⚠️ Keys are the ones these two nodes ALREADY hold, read from their own /status. Their host
36
+ # secrets are untouched and stay where they are — only the public halves appear here.
37
+ {
38
+ node-id = "evolvermeerkatnode920080"
39
+ host = "65.21.255.224"
40
+ port = 7100
41
+ public-key = "MCowBQYDK2VwAyEAvDNSSDsSYRHfvFCOcl/Fba89VnBrkLzYBy28xqjg8Zg="
42
+ bls-public-key = "iJZMbE+J+TnNnHpYxk2lQDJipTvvUg0qCQNVJViTfoatqZkqazLCZbUmSP4W26Vj"
43
+ quorum-weight = 0
44
+ },
45
+ {
46
+ node-id = "schednode2"
47
+ host = "65.21.255.224"
48
+ port = 7200
49
+ public-key = "MCowBQYDK2VwAyEAvUOp3fA1nTjv546jme0n14wYDVjHshLL3I7XpQxQ8V4="
50
+ bls-public-key = "ht3U8qRoQtKIU0NUbuuwx/ni4iF+rcAs2dJgxDOcf+YOs+brlSKSnC2XBFBp1rR1"
51
+ quorum-weight = 0
52
+ },
53
+
54
+ # ⚠️ devnet-seed-3 IS NEVER DEPLOYED, ON PURPOSE, AND MUST STAY THAT WAY. A list of seeds is
55
+ # only worth having if a dead entry is walked past rather than waited on, and a seed that is
56
+ # permanently absent tests that on every single join instead of on the one afternoon somebody
57
+ # remembers to stop a live one. Its private key material was generated in a throwaway container
58
+ # and destroyed with it — nobody can bring this node up, which is the point. `.invalid` is
59
+ # reserved by RFC 2606 and can never resolve, so the attempt fails fast instead of hanging.
60
+ # If you are reading this because something reports it unreachable: that is the test passing.
61
+ {
62
+ node-id = "devnet-seed-3"
63
+ host = "devnet-seed-3.invalid"
64
+ port = 7300
65
+ public-key = "MCowBQYDK2VwAyEAHY2onS4+fZss0VZaT84AMcQQugILPagvBm3PFDE5cYY="
66
+ bls-public-key = "oO91EyEt4jvpP3XuuNiT+6F/9djCjAqgs/+e7eI64eGz9iRCcHT5HY17coLLKfsk"
67
+ quorum-weight = 0
68
+ }
69
+ ]
70
+
71
+ ce.evolver.consensus {
72
+ # ⚠️ ONE voting node. This is the committee size, not the number of genesis entries — the three
73
+ # seed entries above carry weight 0 and are not counted (NodeBootstrap derives the consensus
74
+ # cluster size with the same `quorumWeight > 0` filter the quorum store and leader rotation
75
+ # already use; before 170 it counted every entry, which would have put the relay threshold
76
+ # beyond anything a one-member committee could reach).
77
+ n-nodes = 1
78
+ block-time-ms = 600
79
+ round-timeout-ms = 1500
80
+ block-reward-tokens = 10000 # generous dev reward: the faucet (5000/identity) is funded from the FIRST blocks, even under quiescence
81
+ snapshot-interval-blocks = 0 # single-node: no snapshot cadence; joiners sync from block 0
82
+ identity-sponsorship-cost = 100
83
+
84
+ # Low idle disk-write: after 3 empty blocks the leader lingers on a 5-MINUTE heartbeat
85
+ # instead of the 600ms cadence (#31: idle-heartbeat-ms > block-time-ms activates quiescence);
86
+ # wake-on-work (node-local, always on) still proposes immediately on incoming txs.
87
+ # CHAIN-WIDE — identical on every node by construction.
88
+ quiescence {
89
+ idle-heartbeat-ms = 300000
90
+ idle-after-empty-rounds = 3
91
+ }
92
+ }
@@ -0,0 +1,270 @@
1
+ #!/bin/sh
2
+ # Claim-then-boot (evolver feature 102), the pattern from the evolver repo's
3
+ # deployments/group-chat/app-node-init.sh, made publishable: nothing is bind-mounted and
4
+ # the node id is derived rather than required, so this artifact runs in an empty
5
+ # directory with no arguments.
6
+ #
7
+ # FIRST boot: generate this node's keys LOCALLY (they never leave this volume),
8
+ # faucet-claim its identity on devnet (the committee node sponsors and funds it — no
9
+ # admin secret exists anywhere in this deployment), persist the result. EVERY boot:
10
+ # source it and start as a non-genesis follower, syncing the chain from block 0.
11
+ set -eu
12
+
13
+ : "${DEVNET_API:?DEVNET_API required (the faucet base url, e.g. https://devnet.mcsherrylabs.com)}"
14
+
15
+ # ⚠️ CHECKED HERE, not left to HOCON. `preferred-url` is a REQUIRED substitution in the
16
+ # node config, so an unset value fails with
17
+ # ConfigException$UnresolvedSubstitution: Could not resolve substitution to a value
18
+ # and a stack trace that says nothing about what an operator must do. Required is the
19
+ # right choice — an unroutable default is what made this node dial its own loopback and
20
+ # stop forwarding submits — but the refusal has to be readable.
21
+ # NOTE: meerkat's version of this script FATALs here unless EVOLVER_TRANSPORT_ADDRESS is set. That is
22
+ # an ANNOUNCER's requirement — its channel has a DA group, so other nodes must be able to dial it
23
+ # to flood bodies to it. A reader has no such need and must advertise nothing, so the guard is
24
+ # deliberately absent rather than satisfied with a made-up value.
25
+
26
+
27
+ KEYS=/data/keys
28
+ CLAIM="$KEYS/claim.env"
29
+ ID_FILE="$KEYS/node-id"
30
+ mkdir -p "$KEYS"
31
+ chmod 700 "$KEYS"
32
+
33
+ # The node id is chosen ONCE and persisted, because it is an on-chain name: re-deriving
34
+ # it on a later boot would claim a SECOND identity and abandon the first.
35
+ if [ -f "$ID_FILE" ]; then
36
+ NODE_ID="$(cat "$ID_FILE")"
37
+ echo "[node-join] reusing node id '$NODE_ID' (chain state continues)"
38
+ else
39
+ if [ -n "${APP_NODE_ID:-}" ]; then
40
+ NODE_ID="$APP_NODE_ID"
41
+ else
42
+ # Identity names are ^[A-Za-z0-9]{1,50}$ — no hyphens, dots or underscores — so the
43
+ # app name is stripped to what a node id may contain (the same rule as
44
+ # scripts/versions.mjs identityPrefix), and a random suffix keeps a shared chain
45
+ # collision-free: a fixed name would come back `exists` with keys nobody holds.
46
+ BASE="$(printf '%s' "${APP_NAME:-app}" | tr -cd '[:alnum:]' | cut -c1-40)"
47
+ [ -n "$BASE" ] || BASE=app
48
+ NODE_ID="${BASE}node$(tr -cd '0-9' < /dev/urandom | head -c 6)"
49
+ fi
50
+ printf '%s' "$NODE_ID" > "$ID_FILE"
51
+ echo "[node-join] node id '$NODE_ID' chosen and persisted"
52
+ fi
53
+
54
+ if [ ! -f "$CLAIM" ]; then
55
+ # A CLAIM WHOSE CONFIRMATION TIMED OUT MUST NOT BE RE-SENT BLIND.
56
+ #
57
+ # Observed on the live devnet: the faucet answered
58
+ # `504 {"error":"confirmation_timeout","message":"Transaction submitted but block
59
+ # confirmation timed out"}` — submitted, outcome UNKNOWN — the container exited, and
60
+ # the next boot found the id file but no claim file and claimed AGAIN. That is the one
61
+ # move that cannot be undone: the client persists its keypair only on success, so the
62
+ # first attempt's keys are gone, and if that attempt committed the name now belongs to
63
+ # keys nobody holds. The retry then either takes a name with a fresh keypair (and the
64
+ # first identity is abandoned) or comes back `exists`, after which the node fails every
65
+ # peer handshake with `Signature does not match` — the 103 failure verbatim.
66
+ #
67
+ # So the outcome is settled by READING, exactly as the app settles a submit: if the
68
+ # name is on chain and we hold no keys for it, stop and say so. The name cannot be
69
+ # reclaimed, so this is terminal by nature and the operator needs a different one.
70
+ # ASK, AND DISTINGUISH "NO" FROM "COULD NOT ASK".
71
+ #
72
+ # `wget | grep -q '"balance"'` is false for BOTH "the name is free" and "the faucet
73
+ # is unreachable", and on this path the second must not be read as the first: the
74
+ # whole point is to avoid claiming a name that may already be bound to keys nobody
75
+ # holds. So the identity route is asked with the exit status kept, and only a body
76
+ # that says `not_found` counts as free.
77
+ # 0 with "balance" -> taken
78
+ # 0 with "not_found" -> free
79
+ # anything else -> could not tell, which is not a licence to claim
80
+ # ⚠️ THE STATUS LINE, NOT THE EXIT CODE. `/api/identity/<name>` answers 404 for a name
81
+ # that is FREE — the commonest case — and `wget -qO-` reports any 404 as exit 8 with no
82
+ # body, so keying on the exit code classified "free" as "cannot ask" and refused to
83
+ # claim anything at all. Caught by the local rig, where every name is free.
84
+ #
85
+ # `-S` prints the response line even for an error status (GNU wget 1.25 in this image),
86
+ # so the three answers are distinguishable:
87
+ # 200 + "balance" -> taken
88
+ # 404 -> free
89
+ # anything else -> could not ask, which is NOT a licence to claim
90
+ id_state() {
91
+ # `|| true` IS LOAD-BEARING, and it is the second layer of the same trap. Keying on
92
+ # the status line instead of the exit code is necessary but not sufficient: under
93
+ # `set -e` the ASSIGNMENT inherits wget's exit status, so a 404 killed this function
94
+ # before the case could run — the caller saw an empty answer and the script exited 8.
95
+ _resp="$(wget -S -qO- "$DEVNET_API/api/identity/$1" 2>&1)" || true
96
+ case "$_resp" in
97
+ *"200 OK"*'"balance"'*) echo taken ;;
98
+ *"200 OK"*) echo taken ;;
99
+ *"404"*) echo free ;;
100
+ *) echo unreachable ;;
101
+ esac
102
+ }
103
+ state="$(id_state "$NODE_ID")"
104
+ if [ "$state" = unreachable ]; then
105
+ echo "[node-join] FATAL: cannot reach $DEVNET_API to ask whether '$NODE_ID' is already"
106
+ echo "[node-join] claimed. Refusing to claim blind: if that name is already bound to keys"
107
+ echo "[node-join] this deployment does not hold, claiming it again destroys the name."
108
+ exit 1
109
+ fi
110
+ if [ "$state" = taken ]; then
111
+ echo "[node-join] FATAL: identity '$NODE_ID' already exists on chain, and this"
112
+ echo "[node-join] deployment holds no keys for it ($CLAIM is absent). A previous"
113
+ echo "[node-join] claim committed after its confirmation timed out, and its keys"
114
+ echo "[node-join] were never persisted — nothing here can authenticate as that name."
115
+ echo "[node-join] Set APP_NODE_ID to an unused name (an identity name cannot be reused)."
116
+ exit 1
117
+ fi
118
+ echo "[node-join] first boot: claiming identity '$NODE_ID' via $DEVNET_API"
119
+ java -cp "/opt/evolver/lib/*" ce.evolver.tools.IdentityClaimClient \
120
+ --target="$DEVNET_API" --node-id="$NODE_ID" --out="$CLAIM" \
121
+ --timeout="${CLAIM_TIMEOUT:-180}" || CLAIM_RC=$?
122
+ if [ ! -f "$CLAIM" ]; then
123
+ # OUR attempt's outcome is now the unknown one, and asking ONCE is not enough: a
124
+ # `504 confirmation_timeout` means the transaction was submitted, and it can commit
125
+ # minutes later (its TTL is +50 rounds, and on a quiescent chain rounds are slow).
126
+ # Asking immediately and printing "the name is still free" was wrong while the claim
127
+ # was in flight — and with `restart: unless-stopped` the next boot would claim the
128
+ # same name AGAIN with fresh keys, which is how a name gets bound to keys nobody
129
+ # holds. So watch it for a bounded window before saying anything.
130
+ echo "[node-join] the claim for '$NODE_ID' did not confirm (exit ${CLAIM_RC:-?}) —"
131
+ echo "[node-join] outcome UNKNOWN. Watching for up to ${CLAIM_WATCH_SECS:-300}s to see"
132
+ echo "[node-join] whether it commits; nothing is resent."
133
+ _waited=0
134
+ _final=free
135
+ while [ "$_waited" -lt "${CLAIM_WATCH_SECS:-300}" ]; do
136
+ _s="$(id_state "$NODE_ID")"
137
+ if [ "$_s" = taken ]; then _final=taken; break; fi
138
+ sleep 10
139
+ _waited=$((_waited + 10))
140
+ done
141
+ if [ "$_final" = taken ]; then
142
+ echo "[node-join] FATAL: the claim for '$NODE_ID' COMMITTED after its confirmation"
143
+ echo "[node-join] timed out, and its keys were never persisted (evolver#86)."
144
+ echo "[node-join] Set APP_NODE_ID to an unused name and start again — an identity"
145
+ echo "[node-join] name cannot be reused."
146
+ else
147
+ echo "[node-join] '$NODE_ID' is still not on chain after ${_waited}s, so the claim"
148
+ echo "[node-join] did not commit and the name remains free; a restart may retry it."
149
+ fi
150
+ exit 1
151
+ fi
152
+ chmod 600 "$CLAIM"
153
+ echo "[node-join] claim committed — keys persisted (mode 600)"
154
+ fi
155
+
156
+ # EVOLVER_PRIVATE_KEY / EVOLVER_NODE_PUBLIC_KEY / EVOLVER_BLS_SEED
157
+ set -a
158
+ . "$CLAIM"
159
+ set +a
160
+
161
+ mkdir -p /data/chain
162
+ # ⚠️ THE AGENT WRITES HERE TOO, as uid 1000, not as this node's uid (10001). Without o+rwx on
163
+ # the directory the agent gets EACCES creating the file at all. The Docker socket already grants
164
+ # equivalent access (docker exec), so this does not widen the effective boundary.
165
+ chmod o+rwx /data/chain
166
+
167
+ # ── WHICH CHANNELS THIS NODE DECLARES ─────────────────────────────────────
168
+ #
169
+ # ONE decision, in ONE place, keyed on the SAME switch the app uses. The node config
170
+ # `include required`s this file, so if the write below ever fails the node refuses to
171
+ # start instead of quietly declaring nothing (HOCON ignores an absent optional include,
172
+ # and a node that declares no channels retains no bodies, streams nothing and delivers
173
+ # nothing — while logging nothing unusual).
174
+ #
175
+ # The registry's channel is declared IFF a registry is deployed here. Declaring it
176
+ # otherwise wedges this node's apply on the first transaction written to a channel it is
177
+ # not a DA member of: nothing stores the body, nothing can pull it, and apply is
178
+ # forward-only (evolver#83).
179
+ # ⚠️ A SUBSCRIPTION IS HOW THIS COMPONENT GAINS A CAPABILITY, not a configuration detail. Each
180
+ # ledger declared here is something the component can then process, index and act on — the set grows
181
+ # as its owner's interests do.
182
+ #
183
+ # The precondition is not optional: this node must be able to OBTAIN the bodies. A channel with a DA
184
+ # policy serves them from its DA group, so this node needs a live connection to a member — otherwise
185
+ # apply blocks on the first body it cannot fetch, PERMANENTLY, because apply is forward-only
186
+ # (evolver#83). For `ce.meerkat.announcements` that group is `evolvermeerkatA286174reg`, whose only
187
+ # member is `appnode046550` — which this node discovers and dials.
188
+ #
189
+ # ⚠️ THIS LIST IS MERGED, NEVER REPLACED. `meerkat_add_channel` writes the same file when the
190
+ # agent subscribes to something, so the file is the union of two writers: this deployment's
191
+ # channel, which is a property of the deployment, and whatever the agent has since subscribed
192
+ # to, which is a property of its work. A boot that rewrote the list from EVOLVER_CHANNEL alone
193
+ # would silently revoke every capability the agent had gained — including the reply channel it
194
+ # is waiting on — and the agent has no way to notice: the file it wrote is simply gone.
195
+ #
196
+ # ⚠️ AND IT IS WRITTEN tmp-THEN-RENAME, chmod 666. Two uids write this one file, and a plain
197
+ # `>` redirect must open the EXISTING file for writing: whichever of them creates it owns a
198
+ # 0644 the other then cannot truncate, for as long as the file lives. That is not theoretical —
199
+ # it wedged a node in a restart loop (`cannot create /data/chain/channels.conf: Permission
200
+ # denied`), and because `include required` makes the node refuse to start rather than declare
201
+ # nothing, the whole stack came down with it. `mv` needs write on the DIRECTORY, which the
202
+ # o+rwx above grants both, so the rename lands whoever owns the old file; the 666 then lets
203
+ # the agent's direct `writeFileSync` land too.
204
+ # ⚠️ THE MCP CONSUMES NO CHANNEL STREAM, so the node declares NO channel by default
205
+ # (`EVOLVER_CHANNEL` defaults empty). The `include required` of `channels.conf` in node.conf still
206
+ # needs the file to EXIST, so an empty list is written below rather than the file being omitted.
207
+ _ch="${EVOLVER_CHANNEL:-}"
208
+ _conf=/data/chain/channels.conf
209
+ _existing=""
210
+ if [ -f "$_conf" ]; then
211
+ # the HOCON this file ever contains is one list on one line, written by this script or by
212
+ # pi-meerkat's formatHocon — strip to the bare comma-separated ids.
213
+ _existing=$(sed -n 's/.*\[\(.*\)\].*/\1/p' "$_conf" | tr -d ' "')
214
+ fi
215
+ _merged="$_ch"
216
+ for _c in $(echo "$_existing" | tr ',' ' '); do
217
+ [ -n "$_c" ] || continue
218
+ [ "$_c" = "$_ch" ] && continue
219
+ _merged="$_merged,$_c"
220
+ done
221
+ # An empty channel set becomes `[]`, not `[""]` — the node declares no subscribed channel, which
222
+ # is correct for a reader that owns no channel stream.
223
+ if [ -z "$_merged" ] || [ -z "${_merged##*,}" ]; then
224
+ _body="[]"
225
+ else
226
+ _body="[ $(echo "$_merged" | sed 's/,/", "/g; s/^/"/; s/$/"/') ]"
227
+ fi
228
+ echo "[node-join] declaring channels: $_merged"
229
+ printf 'ce.evolver.subscribed-channels = %s\n' "$_body" > "$_conf.node.tmp"
230
+ chmod 666 "$_conf.node.tmp"
231
+ mv -f "$_conf.node.tmp" "$_conf"
232
+
233
+ # ── WHICH PEERS THIS NODE DIALS ───────────────────────────────────────────
234
+ #
235
+ # ONE list rendered as BOTH keys, because they are one decision: `preferred-peers` says
236
+ # who, `allowed-outbound` says how many, and the pin is strict only while the two agree
237
+ # (node.conf says why). Computed here rather than written in node.conf for the same
238
+ # reason the channel list is — so the two halves cannot drift apart.
239
+ #
240
+ # The upstream is always dialled. EVOLVER_DA_PEERS adds the DA members this deployment
241
+ # must reach to PULL bodies for the channels declared above; without a slot for them,
242
+ # `ChannelOverlay.peers()` is empty for those channels and nothing is ever fetched.
243
+ _peers="${EVOLVER_UPSTREAM_NODE_ID}"
244
+ # EVOLVER_DA_PEERS, comma-separated, deduplicated against the upstream. The comment above has
245
+ # promised this since the file was written and the code never did it — so a deployment that set
246
+ # it got silence, and the only way to pin a second peer was to smuggle a comma into
247
+ # EVOLVER_UPSTREAM_NODE_ID, which works here only by accident of raw substitution and breaks the
248
+ # dedupe and the count in the sibling stacks that DO implement this. Same shape as
249
+ # evolver-llm/docker-compose.devnet.yml.
250
+ for _p in $(printf '%s' "${EVOLVER_DA_PEERS:-}" | tr ',' ' '); do
251
+ if [ -n "$_p" ] && [ "$_p" != "${EVOLVER_UPSTREAM_NODE_ID}" ]; then
252
+ _peers="$_peers, $_p"
253
+ fi
254
+ done
255
+ _outbound="${EVOLVER_ALLOWED_OUTBOUND:-16}"
256
+ echo "[node-join] pinning ${_peers} as the seed; allowing ${_outbound} outbound so discovery can happen"
257
+ {
258
+ # The seed is PINNED so there is always a way back to the chain. `allowed-outbound` is
259
+ # deliberately LARGER than that one entry: the pin is strict only while the two are equal, and a
260
+ # strict pin would stop this node dialling anything it discovers. Once it has peers it learns its
261
+ # own distance to the committee and forwards transactions along that path like any other node.
262
+ printf 'evolver.peer-discovery.preferred-peers = [ %s ]
263
+ ' "$_peers"
264
+ printf 'evolver.peer-discovery.allowed-outbound = %s
265
+ ' "$_outbound"
266
+ } > /data/chain/peers.conf
267
+
268
+ echo "[node-join] launching follower '$NODE_ID' — syncing devnet from block 0"
269
+ exec /opt/evolver/bin/evolver -Dconfig.file=/config/node.conf "$NODE_ID"
270
+
@@ -0,0 +1,277 @@
1
+ # This node: a NON-GENESIS FOLLOWER on devnet. Chain-wide params come from the genesis
2
+ # spec — never set them here, they must stay byte-identical on every node. Only
3
+ # node-local knobs below.
4
+ include file("/config/genesis.devnet.conf")
5
+
6
+ # 089 chain-spec pin: the canonical hash of the genesis file IS the genesis block id.
7
+ # Any drift refuses to boot rather than forking quietly.
8
+ ce.evolver.genesis-file = "/config/genesis.devnet.conf"
9
+
10
+ # This node's OWN identity, from the first-boot faucet claim (node-join.sh). REQUIRED
11
+ # substitutions (no `?`): a node booting with missing keys must fail fast, not later and
12
+ # obscurely at the first TLS handshake.
13
+ ce.evolver.node {
14
+ private-key = ${EVOLVER_PRIVATE_KEY} # Ed25519 PKCS#8 base64
15
+ public-key = ${EVOLVER_NODE_PUBLIC_KEY} # Ed25519 X.509 base64
16
+ bls-secret = ${EVOLVER_BLS_SEED} # BLS IKM base64
17
+ port = ${EVOLVER_NODE_PORT} # p2p transport (raw TCP, mTLS)
18
+ }
19
+
20
+ evolver.peer-discovery {
21
+ # THIS NODE ADVERTISES NOTHING, AND MUST NOT. `preferred-url` is the address this node
22
+ # gossips as its OWN (`""` = feature off), and a joined app node behind Docker HAS no
23
+ # routable p2p address: its 7000 is container-local and deliberately unpublished. So
24
+ # advertising one means every node on the chain records `<us> -> https://127.0.0.1:7000`
25
+ # and then dials its own loopback.
26
+ #
27
+ # Observed on devnet, and it is worse than noise: this node CONNECTED TO ITSELF, logged
28
+ # `Peer connected: <self>`, sent its own SyncManager `BlockRangeRequest(4841,4845)`, and
29
+ # TipGossip then reported `rotating to <self> (failed=devnet1-node0)` followed by
30
+ # `stalled (upstream=none)`. After that its submits were never forwarded and every write
31
+ # timed out. The table also keeps the dead: `evolvertemplateappnode965813 at
32
+ # 127.0.0.1:7000` from an earlier deployment was still being dialled hours later.
33
+ # ADVERTISED AGAIN, and ⚠️ ONLY BECAUSE IT IS NOW REAL. This was `""` because the node
34
+ # had been advertising `https://127.0.0.1:7000`: every peer that read the discovery
35
+ # table dialled its OWN loopback, so this node connected to itself, sent its own
36
+ # SyncManager block requests, reported `candidates=<self>` / `stalled (upstream=none)`
37
+ # and stopped forwarding submits. An unroutable value reinstates that exactly — which
38
+ # is why `EVOLVER_TRANSPORT_ADDRESS` is REQUIRED here rather than defaulted, and why the
39
+ # command suite refuses a loopback value.
40
+ # ⚠️ EMPTY, DELIBERATELY. This node is a READER behind NAT: it has no routable p2p address.
41
+ # Advertising one is not merely useless, it is harmful — devnet recorded a node that advertised
42
+ # an unroutable address CONNECTING TO ITSELF, sending its own sync requests, reporting
43
+ # `stalled (upstream=none)`, and leaving dead entries other nodes dialled for hours.
44
+ #
45
+ # It costs this node nothing: advertising is how others DIAL YOU, and nothing needs to. Writing
46
+ # still works — this node dials OUT, discovers the network, learns its distance to the committee
47
+ # and forwards transactions along that path. Being undialable only stops bodies being flooded TO
48
+ # it, which is a DA member's concern and not a reader's.
49
+ transport-address = ""
50
+ peer-url = ""
51
+ gossip-interval-seconds = 60
52
+
53
+ # 16 INBOUND, not 0. The 141 leaf recipe (`in=0`) is right for an app node that only
54
+ # talks upstream — and wrong here: the registry's channel requires announcers' nodes to
55
+ # reach THIS node so their flooded bodies can be attested (research D2). Each announcer
56
+ # holds one inbound slot, so 16 is the number of concurrent announcer nodes supported.
57
+ # Generous on both sides so DISCOVERY actually happens. The seed below is pinned so the node
58
+ # always has a way back to the chain, but `allowed-outbound` is deliberately LARGER than the
59
+ # pinned list — the pin is strict only while the two are equal, and a strict pin here would mean
60
+ # this node never dials anything it discovers.
61
+ allowed-inbound = 16
62
+ # `allowed-outbound` and `preferred-peers` are DELIBERATELY ABSENT HERE — node-join.sh renders
63
+ # both into peers.conf so the two cannot drift apart in two files.
64
+ #
65
+ # ⚠️ THIS COMMENT USED TO CLAIM THE PIN IS STRICT. It is NOT, and the difference matters to
66
+ # anyone reasoning about what this node will dial. It said `|preferred| == allowed-outbound`
67
+ # holds "by construction"; node-join.sh renders ONE pinned seed and an outbound budget of 16,
68
+ # so 1 != 16 and the pin is deliberately LOOSE. That is the intended design — a follower has to
69
+ # dial what it discovers or it is a leaf — but the old wording described the opposite property
70
+ # and named the incidents (a dead address from a torn-down deployment still being dialled, this
71
+ # node dialling its own loopback) that a strict pin was supposed to prevent. Those are the COST
72
+ # of the loose pin, not evidence against it: the seed guarantees a way back to the chain, and
73
+ # discovery does the rest. Which is what keeps a shared chain's discovery table out of the
74
+ # dial queue — it keeps the dead (an address from a torn-down deployment was still
75
+ # being dialled hours later) and once made this node dial its own loopback. Raising
76
+ # the budget without naming a peer would silently unpin it; naming a peer without
77
+ # raising the budget could never reach it. One list, both keys, no drift.
78
+ #
79
+ # ⚠️ THIS USED TO BE `out=1`, copied verbatim from the 141 LEAF recipe, and it decided
80
+ # WHICH CHANNELS THIS NODE COULD READ — silently. `ChannelOverlay.peers(ledgerId)` is
81
+ # the channel's DA group ∩ connected, so with the one slot spent on the upstream every
82
+ # channel whose DA member is somebody else resolved to an EMPTY pull set: the node
83
+ # records the miss, sends no request, and says so at DEBUG only (evolver#89). A node
84
+ # reading a channel it does not itself serve needs the upstream AND one reachable DA
85
+ # member per such channel, so a fixed 1 is only ever right for a node that reads
86
+ # nothing but its own.
87
+ }
88
+
89
+ ce.evolver.consensus {
90
+ validate-chain-on-startup = false
91
+ wake-on-work { forward-watermark = 16 }
92
+ }
93
+
94
+ # PROCESSING MODE — off by default, and THE REASON THIS DEPLOYMENT EXISTS once your app
95
+ # reads a channel.
96
+ #
97
+ # `/api/channel/next` is a SINGLE-CONSUMER, FORWARD-ONLY cursor: two readers do not
98
+ # error, they silently split the stream and the message is unrecoverable. So an app that
99
+ # consumes an ordered channel must drain a node NOBODY ELSE drains — which means a node
100
+ # of its own. It cannot share a public one, and a single-node chain (devnet today) IS the
101
+ # committee, so that node is not a processing node and serves no stream at all.
102
+ #
103
+ # And the committee's node must never be put in this mode: processing mode DEFERS apply
104
+ # and paces it from the app's polling, so a committee node in processing mode would
105
+ # couple consensus progress to an HTTP poller. Ordering is its job; give the app its own.
106
+ #
107
+ # Turning it on changes what committed reads MEAN here — they answer "as of MY cursor".
108
+ # Measured in this family (evolver-scheduler): right after a successful faucet claim a
109
+ # quorum node reported `{"balance":5000}` for an identity while a processing node
110
+ # reported `{"error":"not_found"}` for the same one. An app in that mode needs to know
111
+ # which of its questions are "has the chain agreed?" (ask a quorum node) and which are
112
+ # "what have I processed?" (ask this one).
113
+ #
114
+ # ON, because this app owns a channel (`announcements`) and reads its ordered stream —
115
+ # steps 5-7 of src/app.ts. BOTH lines are needed and neither works alone: a node in
116
+ # processing mode with no channels declared has nothing to offer, and a node that
117
+ # declares channels without execution stores bodies it never streams.
118
+ #
119
+ # The channel must be named HERE as well as claimed on chain. A processing node stores
120
+ # bodies only for the channels it lists, so an undeclared channel's message is applied
121
+ # in the slice and never offered — the write looks fine and the stream stays silent,
122
+ # which is the failure the app's step 7 names explicitly.
123
+ # ⚠️ OFF. This is a READER, and processing mode is what a node running an APP turns on.
124
+ #
125
+ # In processing mode apply is DEFERRED and paced by the app polling /api/channel/next, so
126
+ # committed reads answer "as of MY cursor" rather than "as of the chain". With no channels
127
+ # declared and no consumer draining — which is exactly this node — the cursor never advances and
128
+ # NOTHING IS EVER APPLIED: the node syncs every block and still answers `not_found` for an identity
129
+ # the chain committed. Observed here: its own identity, committed at block 7225 and verified on
130
+ # chain, was unknown to it at height 7231.
131
+ #
132
+ # ⚠️ ON, NOW THAT SOMETHING DRAINS THIS NODE. The rule is not "readers off, apps on" — it is that
133
+ # processing mode paces apply from whatever drains /api/channel/next, so it is correct exactly when
134
+ # there IS a drainer and catastrophic when there is not (with none, this node synced every block and
135
+ # still answered `not_found` for an identity it had itself committed).
136
+ #
137
+ # The meerkat registry beside it is that drainer. Its polling is what advances apply, so the agent's
138
+ # reads stay correct — as of that cursor, which is what a component reading its own processed state
139
+ # means by correct.
140
+ ce.evolver.execution.enabled = ${?EVOLVER_EXECUTION_ENABLED}
141
+ # ⚠️ THE CHANNEL LIST IS GENERATED BY node-join.sh, and `include required` means this node
142
+ # REFUSES TO START if the fragment is missing rather than silently declaring nothing.
143
+ #
144
+ # It cannot be written here, because WHICH channels this node declares depends on whether
145
+ # a registry is deployed beside it — and declaring one that is not wanted is not harmless:
146
+ #
147
+ # the registry's channel is declared by the node so it can STORE and ATTEST bodies
148
+ # (`onBody` drops a flooded body for an undeclared channel, and a body it never stored
149
+ # is one it cannot attest — at m=1 that stops the registry rather than degrading it);
150
+ #
151
+ # but a node that declares a channel it is NOT a DA member of STALLS at every transaction
152
+ # on that channel. `channelSubscribes` is false, so it stores no body, and nothing can be
153
+ # pulled either (the DA group's members are identities, not this node's peers), so the
154
+ # position answers `body-pending` and the cursor sits ON it — applied state, and every
155
+ # committed read, frozen at the block before it.
156
+ #
157
+ # ⚠️ NOT a permanent wedge of the NODE, and evolver#83 was corrected on exactly this
158
+ # point: the runner advances the cursor atomically with the apply and the body lookup
159
+ # happens afterwards, so a CONSUMER that asks for `index + 1` walks past and the skipped
160
+ # transaction is still applied in the slice. What is permanent is the LOSS of that
161
+ # message. So the stall lasts as long as the consumer retries the same position — which
162
+ # the SDK's `drainChannel` does, correctly, for at-most-once delivery — and it is
163
+ # reachable by anyone willing to spend 10 credits on an id-only write to an unclaimed
164
+ # name. Declaring only what this deployment participates in avoids the whole question.
165
+ #
166
+ # In demo mode this node is not in the registry channel's DA group — nothing here claimed
167
+ # it — so declaring it was exactly that hazard. The switch is PROCESSOR_HTTP_PORT (named
168
+ # REGISTRY_HTTP_PORT before meerkat v0.6.0), the same one the app uses to decide it is a
169
+ # service, so the two cannot disagree.
170
+ include required(file("/data/chain/channels.conf"))
171
+ # `required` for the same reason: an absent optional include is IGNORED, and a node
172
+ # that silently fell back to the 64/64 defaults would dial the discovery table.
173
+ include required(file("/data/chain/peers.conf"))
174
+
175
+ # ── THE LOCAL ROUTES THIS DEPLOYMENT OPENS TO ITS OWN CONTAINERS ────────────
176
+ #
177
+ # The agent and the registry run BESIDE this node, each in its own container on this compose
178
+ # project's network. These routes carry no token and no client certificate — reaching the address
179
+ # IS the authorization — so the node binds them to loopback unless told otherwise, and loopback
180
+ # inside this container is reachable by nothing else in the stack.
181
+ #
182
+ # `bind-host = 0.0.0.0` here means: THIS COMPOSE NETWORK IS THE BOUNDARY. That claim holds only
183
+ # because none of these ports appear under `ports:` in docker-compose.yml and the stack does not
184
+ # use host networking. Publish one, or run with `--network host`, and the claim is simply false.
185
+ #
186
+ # ⚠️ `ce.evolver.login` is deliberately NOT here and cannot be: that route signs as any identity
187
+ # this host holds a signer for, which on this node is the node's own. Setting `bind-host` under
188
+ # that namespace fails the boot rather than being ignored.
189
+
190
+ # 145 — the agent sends and drains application messages. `/api/p2p/send` relays a message the
191
+ # agent signed ITSELF (its own identity, verified at the far end); `/api/p2p/sign-and-send` makes
192
+ # this node sign as itself. One listener, so opening it grants both.
193
+ ce.evolver.p2p {
194
+ http-port = 8460
195
+ bind-host = "0.0.0.0"
196
+ }
197
+
198
+ # 158 — the agent may ASK an enrolled phone to authorise a transaction. It cannot sign: the device
199
+ # holder approves every one, and that gate does not move when the bind does. Inert until a
200
+ # `mobile-signer` block exists, and the node says so at boot rather than failing.
201
+ ce.evolver.mobile {
202
+ http-port = 8470
203
+ bind-host = "0.0.0.0"
204
+ }
205
+
206
+ # 139 — CONTENT, so the agent can publish a document and the registry can read one back.
207
+ #
208
+ # `POST /content` writes to this node's own store, keyed by the bytes' own digest, and talks to
209
+ # no peer at all (FR-006). `bind-host` below is what lets the agent and the registry reach that
210
+ # route from their own containers. Fetching a document another node holds is the part that uses
211
+ # the overlay, and that is what the TLS listener underneath is for.
212
+ ce.evolver.content {
213
+ enabled = true
214
+ http-port = 8450
215
+ bind-host = "0.0.0.0"
216
+ }
217
+
218
+ data.base = "/data/chain"
219
+ state.snapshots = "snapshots"
220
+ # BOTH LISTENERS, and the pair is the point.
221
+ #
222
+ # 8440 PLAIN is what the agent and the registry talk to. They are ordinary HTTP clients in their
223
+ # own containers, and the alternative is not "use TLS" — this node's HTTPS listener presents its
224
+ # own IDENTITY certificate, self-signed, CN=<nodeId>, ~13 minutes old. It rotates, so it cannot
225
+ # be pinned as a CA, so a client that verifies it must turn verification off process-wide. For
226
+ # the agent that would also stop it verifying GoToSocial, which is not a trade worth making for
227
+ # a hop between two containers on one network.
228
+ #
229
+ # 8441 TLS is what PEERS use, and it exists so `ce.evolver.content` above can: the content peer
230
+ # route identifies its requester by client certificate, and refuses to start without somewhere
231
+ # mTLS to live. NOT PUBLISHED in docker-compose.yml — this node is a reader behind NAT that
232
+ # fetches outbound and serves nobody, so nothing needs to dial it.
233
+ #
234
+ # ⚠️ IF THIS NODE EVER SERVES CONTENT, `evolver.peer-discovery.api-url` must name 8441 and that
235
+ # port must be publicly reachable. It is empty here (see above): a node with no routable address
236
+ # advertises nothing and can only fetch.
237
+ http.port = 8440
238
+ # Only this private Compose network may reach the unsigned processing API.
239
+ http.bind-host = "0.0.0.0"
240
+ # ⚠️ REQUIRED SINCE 131 (#131), and without it this stack breaks. `http.bind-host` now defaults
241
+ # to 127.0.0.1 — the plain listener joined the bind-is-authorization set the four local listeners
242
+ # above already belong to, because it carries the whole API with no token and no client
243
+ # certificate, including (this node runs `ce.evolver.execution.enabled`) the SINGLE-CONSUMER
244
+ # cursor `GET /api/channel/next`, which a stranger can step past the registry's own deliveries.
245
+ #
246
+ # The claim being made here is the same one `ce.evolver.content.bind-host` and the p2p/login ones
247
+ # above make: THIS COMPOSE NETWORK IS THE BOUNDARY. It holds because 8440 is NOT published in
248
+ # docker-compose.yml and nothing uses host networking — the agent and the registry reach this
249
+ # node as `node:8440`, a service name on that network. Publish the port, or run with host
250
+ # networking, and the claim stops being true. The node cannot tell, so it logs a WARN naming it.
251
+ http.bind-host = "0.0.0.0"
252
+ https.port = 8441
253
+ evolver.grpc.port = 9000
254
+
255
+ # REQUIRED, and there is no default: `NodeBootstrap.initChainDb` reads this block
256
+ # directly, so a node without it dies with `No configuration setting found for key
257
+ # 'chainDb'` after the migrations have already run — which is exactly how this omission
258
+ # was found. Same values as the bundled deployment's node.
259
+ chainDb {
260
+ datasource {
261
+ driver = "org.hsqldb.jdbc.JDBCDriver"
262
+ user = "sa"
263
+ pass = ""
264
+ maxPoolSize = 5
265
+ connectionProperties = ""
266
+ cachePrepStmts = true
267
+ prepStmtCacheSize = 250
268
+ prepStmtCacheSqlLimit = 2048
269
+ connectionTimeout = 5000
270
+ useServerPrepStmts = false
271
+ transactionIsolationLevel = "TRANSACTION_READ_COMMITTED"
272
+ }
273
+ viewCachesSize = 50
274
+ useShutdownHook = false
275
+ freeBlobsEarly = false
276
+ }
277
+
@@ -0,0 +1,104 @@
1
+ # Evolver devnet-follower NODE + the evolver-mcp stdio server's launch context.
2
+ #
3
+ # One `docker compose -f deploy/docker-compose.yml up -d` starts a LOCAL Evolver node that
4
+ # JOINS and syncs the live devnet chain (it is a non-genesis follower, not a new chain), and the
5
+ # env contract an agent runtime supplies when it launches `npx -y @mcsherrylabs/evolver-mcp`
6
+ # against that node.
7
+ #
8
+ # CONTENT LIVES HERE, NOT ON DEVNET. The MCP posts to and reads from THIS node's own content
9
+ # cache (`ce.evolver.content.http-port = 8450`) via `EVOLVER_CONTENT_URL=http://node:8450`. The
10
+ # devnet URL is only the FAUCET origin for the node's first-boot identity claim; every other call
11
+ # is against the local node (`node:8440` chain API, `node:8450` content).
12
+ #
13
+ # GROUNDING — mirror /workspace/evolver/deployments/claude-agent/, the canonical follower pattern:
14
+ # - genesis.devnet.conf is a BYTE-IDENTICAL copy of the devnet genesis (committee + seed nodes).
15
+ # It is the chain's identity; never edit it.
16
+ # - node-join.sh is the claim-then-boot entrypoint: mint node keys locally, faucet-claim the
17
+ # identity on devnet (the committee sponsors it), persist the claim, then start as a non-genesis
18
+ # follower syncing from block 0.
19
+ # - node.conf includes genesis + peers.conf + channels.conf; content 8450, chain API 8440, TLS
20
+ # 8441 (peer content fetches), gRPC 9000.
21
+ #
22
+ # THE MCP IS STDIO, NOT A LONG-RUNNING SERVICE. There is no published evolver-mcp image; an agent
23
+ # runtime (Claude Code / Codex) launches it per session as a child process. The `mcp` service below
24
+ # only documents the exact env contract (see the comment on it). For dockerized use, build from
25
+ # docs/mcp.Dockerfile and exec `docker compose run --rm mcp npx -y @mcsherrylabs/evolver-mcp …`.
26
+
27
+ name: evolver-mcp
28
+
29
+ services:
30
+ # ── the LOCAL devnet follower ──────────────────────────────────────────────
31
+ node:
32
+ image: ${EVOLVER_REGISTRY:-nexus.mcsherrylabs.com}/evolver-node:${EVOLVER_VERSION:-v0.24.5}
33
+ restart: unless-stopped
34
+ environment:
35
+ # The node claims its OWN identity (node keys never leave this container) and persists the
36
+ # name to /data/keys/node-id. `<APP_NAME>node<random6>` keeps a shared chain collision-free.
37
+ APP_NAME: evolverscp
38
+ # FAUCET ONLY — the first-boot claim target. Not the chain URL the MCP talks to.
39
+ DEVNET_API: ${DEVNET_API:-https://devnet.mcsherrylabs.com}
40
+ # THE MCP READS NO CHANNEL STREAM, so the node declares NO channel. Empty on purpose: a node
41
+ # that declares a channel it is not a DA member of stalls its apply (evolver#83).
42
+ EVOLVER_CHANNEL: ${EVOLVER_CHANNEL:-}
43
+ # ⚠️ PIN A FRONT/SEED NODE, NOT THE COMMITTEE. `devnet1-node0` runs `allowed-inbound = 2`
44
+ # with both slots reserved; pinning it cannot work (every dial is refused).
45
+ EVOLVER_UPSTREAM_NODE_ID: ${EVOLVER_UPSTREAM_NODE_ID:-evolvermeerkatnode920080}
46
+ EVOLVER_DA_PEERS: ${EVOLVER_DA_PEERS:-schednode2}
47
+ EVOLVER_ALLOWED_OUTBOUND: ${EVOLVER_ALLOWED_OUTBOUND:-16}
48
+ # ⚠️ PROCESSING MODE STAYS OFF. Apply would be paced by whatever drains /api/channel/next;
49
+ # with no drainer the cursor never advances and committed reads answer not_found. A reader
50
+ # with no channel owns no stream, so leave this unset (the config's `?` makes it optional).
51
+ #
52
+ # (Deliberately ABSENT, not "false": the config reads `ce.evolver.execution.enabled = ${?EVOLVER_EXECUTION_ENABLED}`
53
+ # — unset means "off", which is exactly a reader node.)
54
+ # p2p transport on 7100, NOT 7000 (most devnet discovery entries advertise 127.0.0.1:7000,
55
+ # which a node on 7000 would dial into its own loopback).
56
+ EVOLVER_NODE_PORT: "7100"
57
+ volumes:
58
+ - node-data:/data
59
+ - ./config:/config:ro
60
+ # INVOKED VIA /bin/sh (a bind-mounted script's exec bit depends on the checkout's file mode).
61
+ entrypoint: ["/bin/sh", "/config/node-join.sh"]
62
+ healthcheck:
63
+ test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8440/api/explorer/blocks?limit=1 >/dev/null || exit 1"]
64
+ interval: 10s
65
+ timeout: 5s
66
+ retries: 30
67
+ start_period: 30s
68
+ # ⚠️ PUBLISHED LOOPBACK-ONLY (B1: the frictionless single-box install path). The host-side
69
+ # `npx … install` bootstrap reaches the node at http://127.0.0.1:8440 (chain API) and
70
+ # http://127.0.0.1:8450 (content) after `up -d node`. Nothing leaves the machine: `host_ip:
71
+ # 127.0.0.1` keeps both listeners local, and the node's p2p/TLS listeners stay unpublished
72
+ # (they are for PEERS, reached over the compose network's DNS).
73
+ ports:
74
+ - target: 8440
75
+ published: "8440"
76
+ host_ip: 127.0.0.1
77
+ protocol: tcp
78
+ - target: 8450
79
+ published: "8450"
80
+ host_ip: 127.0.0.1
81
+ protocol: tcp
82
+
83
+ # ── the MCP launch context (documentation only) ─────────────────────────────
84
+ # evolver-mcp is a STDIO server launched per session by an agent runtime — NOT a long-running
85
+ # compose service. This service exists to (a) pin the exact env contract and (b) give a
86
+ # containerized operator a `docker compose run --rm mcp …` target. `depends_on` documents that
87
+ # the node must be healthy before an MCP launch, since the MCP probes and exits if unreachable.
88
+ mcp:
89
+ image: node:22
90
+ profiles: ["run-only"] # not started by `up -d`; a run-target only
91
+ environment:
92
+ # Chain API — the LOCAL node, not devnet.
93
+ EVOLVER_MCP_CONFIG: "/state/config.json"
94
+ # Content listener — the LOCAL node's content cache, a SEPARATE port from the chain API.
95
+ EVOLVER_CONTENT_URL: "http://node:8450"
96
+ EVOLVER_KEY_PATH: "/state/<identity>.key"
97
+ EVOLVER_KEY_TYPE: "bls"
98
+ depends_on:
99
+ node:
100
+ condition: service_healthy
101
+ restart: "no"
102
+
103
+ volumes:
104
+ node-data:
package/dist/cli.js CHANGED
@@ -12,9 +12,28 @@
12
12
  // checkout-entry form Phase 7 locked in.
13
13
  import { main } from "./index.js";
14
14
  import { hasBootstrapFlag, runBootstrap } from "./bootstrap.js";
15
+ import { runInstall } from "./install.js";
15
16
  const PACKAGE_NAME = "@mcsherrylabs/evolver-mcp";
16
17
  const BOOTSTRAP_ENTRY_COMMAND = () => `npx -y ${PACKAGE_NAME}`;
18
+ /** True when the first non-flag argv token is the `install` subcommand. */
19
+ function isInstallCommand(argv) {
20
+ const i = argv.findIndex((a) => !a.startsWith("-"));
21
+ return i >= 0 && argv[i] === "install";
22
+ }
17
23
  await (async () => {
24
+ if (isInstallCommand(process.argv)) {
25
+ // Package mode for the install: the printed registration resolves the published package.
26
+ const env = { ...process.env, EVOLVER_BOOTSTRAP_CWD: process.cwd() };
27
+ if (env.EVOLVER_MCP_ENTRY_COMMAND === undefined) {
28
+ env.EVOLVER_MCP_ENTRY_COMMAND = BOOTSTRAP_ENTRY_COMMAND();
29
+ }
30
+ const r = await runInstall(process.argv, env);
31
+ if (r.toStderr)
32
+ console.error(r.output);
33
+ else
34
+ process.stdout.write(r.output);
35
+ process.exit(r.exitCode);
36
+ }
18
37
  if (hasBootstrapFlag(process.argv)) {
19
38
  // Package mode: only if the operator has not already chosen an entry command, default the
20
39
  // registration to `npx -y @mcsherrylabs/evolver-mcp` so the printed `claude mcp add` resolves
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,0FAA0F;AAC1F,EAAE;AACF,gGAAgG;AAChG,2FAA2F;AAC3F,EAAE;AACF,4FAA4F;AAC5F,+FAA+F;AAC/F,6FAA6F;AAC7F,iGAAiG;AACjG,4FAA4F;AAC5F,yCAAyC;AAEzC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,MAAM,YAAY,GAAG,2BAA2B,CAAC;AACjD,MAAM,uBAAuB,GAAG,GAAG,EAAE,CAAC,UAAU,YAAY,EAAE,CAAC;AAE/D,MAAM,CAAC,KAAK,IAAI,EAAE;IAChB,IAAI,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,0FAA0F;QAC1F,8FAA8F;QAC9F,8CAA8C;QAC9C,MAAM,GAAG,GAAsB,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,qBAAqB,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QACxF,IAAI,GAAG,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAChD,GAAG,CAAC,yBAAyB,GAAG,uBAAuB,EAAE,CAAC;QAC5D,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;;YACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,0FAA0F;AAC1F,EAAE;AACF,gGAAgG;AAChG,2FAA2F;AAC3F,EAAE;AACF,4FAA4F;AAC5F,+FAA+F;AAC/F,6FAA6F;AAC7F,iGAAiG;AACjG,4FAA4F;AAC5F,yCAAyC;AAEzC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,MAAM,YAAY,GAAG,2BAA2B,CAAC;AACjD,MAAM,uBAAuB,GAAG,GAAG,EAAE,CAAC,UAAU,YAAY,EAAE,CAAC;AAE/D,2EAA2E;AAC3E,SAAS,gBAAgB,CAAC,IAAc;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,IAAI,EAAE;IAChB,IAAI,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,yFAAyF;QACzF,MAAM,GAAG,GAAsB,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,qBAAqB,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QACxF,IAAI,GAAG,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAChD,GAAG,CAAC,yBAAyB,GAAG,uBAAuB,EAAE,CAAC;QAC5D,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC9C,IAAI,CAAC,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;;YACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC;IAED,IAAI,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,0FAA0F;QAC1F,8FAA8F;QAC9F,8CAA8C;QAC9C,MAAM,GAAG,GAAsB,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,qBAAqB,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QACxF,IAAI,GAAG,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAChD,GAAG,CAAC,yBAAyB,GAAG,uBAAuB,EAAE,CAAC;QAC5D,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;;YACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC,EAAE,CAAC"}
@@ -0,0 +1,21 @@
1
+ interface InstallEnv {
2
+ /** Override the shipped compose file path (default: `../deploy/docker-compose.yml` beside dist). */
3
+ EVOLVER_INSTALL_COMPOSE?: string;
4
+ /** Health-poll ceiling in ms (default 120000). */
5
+ EVOLVER_INSTALL_TIMEOUT_MS?: string;
6
+ /** Node chain-API URL to poll (default LOCAL_CHAIN_URL). Override for a non-loopback topology. */
7
+ EVOLVER_INSTALL_HEALTH_URL?: string;
8
+ }
9
+ export interface InstallResult {
10
+ readonly exitCode: number;
11
+ readonly output: string;
12
+ readonly toStderr: boolean;
13
+ }
14
+ /** Resolve the shipped `deploy/docker-compose.yml`, honoring an explicit override. */
15
+ export declare function composePathFor(env: InstallEnv, distDir: string): string;
16
+ /** The dist dir this module runs from (src/ → dist/ at build time; dist/cli.js is the bin). */
17
+ export declare function distDirFor(importMetaUrl: string): string;
18
+ /** Spin up the node, wait healthy, then delegate to the bootstrap for the agent identity. */
19
+ export declare function runInstall(argv: string[], env: NodeJS.ProcessEnv & InstallEnv): Promise<InstallResult>;
20
+ export {};
21
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAwBA,UAAU,UAAU;IAClB,oGAAoG;IACpG,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kDAAkD;IAClD,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,kGAAkG;IAClG,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIvE;AAED,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,6FAA6F;AAC7F,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,UAAU,GAClC,OAAO,CAAC,aAAa,CAAC,CAkDxB"}
@@ -0,0 +1,96 @@
1
+ // Frictionless install: start the LOCAL devnet-follower node as a Docker container, wait for it
2
+ // to be healthy, then bootstrap the SEPARATE agent identity against it and print the registration.
3
+ //
4
+ // The node is `deploy/docker-compose.yml` (a non-genesis follower that joins + syncs devnet). The
5
+ // node claims its OWN host identity (`node-join.sh` → faucet), distinct from the agent identity the
6
+ // bootstrap mints here. Content is read/posted through the local node's listener (`:8450`), never
7
+ // devnet directly — devnet is only the faucet origin for the node's first-boot claim.
8
+ //
9
+ // Reachability (B1): the compose publishes the node's 8440/8450 loopback-only, so THIS host-side
10
+ // process reaches `http://127.0.0.1:8440` / `:8450`. The bootstrap then derives
11
+ // `EVOLVER_CONTENT_URL` from the node's content port (see bootstrap's local-target derivation).
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { runBootstrap } from "./bootstrap.js";
17
+ /** The node's chain-API host the install bootstrap targets, per B1 (loopback publish). */
18
+ const LOCAL_NODE_HOST = "127.0.0.1";
19
+ const LOCAL_CHAIN_URL = `http://${LOCAL_NODE_HOST}:8440`;
20
+ const HEALTH_PATH = "/api/explorer/blocks?limit=1";
21
+ /** Resolve the shipped `deploy/docker-compose.yml`, honoring an explicit override. */
22
+ export function composePathFor(env, distDir) {
23
+ const explicit = env.EVOLVER_INSTALL_COMPOSE?.trim();
24
+ if (explicit)
25
+ return resolve(explicit);
26
+ return resolve(join(distDir, "..", "deploy", "docker-compose.yml"));
27
+ }
28
+ /** The dist dir this module runs from (src/ → dist/ at build time; dist/cli.js is the bin). */
29
+ export function distDirFor(importMetaUrl) {
30
+ return dirname(fileURLToPath(importMetaUrl));
31
+ }
32
+ /** Spin up the node, wait healthy, then delegate to the bootstrap for the agent identity. */
33
+ export async function runInstall(argv, env) {
34
+ const composeFile = composePathFor(env, dirname(fileURLToPath(import.meta.url)));
35
+ if (!existsSync(composeFile)) {
36
+ return {
37
+ exitCode: 1,
38
+ toStderr: true,
39
+ output: `install: the node compose file is missing at ${composeFile} — the published package must ship deploy/ (see package.json "files").\n`,
40
+ };
41
+ }
42
+ // 1. Start the follower node (claims its own identity on devnet, then syncs from block 0).
43
+ const up = spawnSync("docker", ["compose", "-f", composeFile, "up", "-d", "node"], {
44
+ encoding: "utf8",
45
+ env: { ...process.env, ...env },
46
+ });
47
+ if (up.status !== 0) {
48
+ return {
49
+ exitCode: 1,
50
+ toStderr: true,
51
+ output: `install: docker compose up -d node failed:\n${up.stderr || up.stdout}`,
52
+ };
53
+ }
54
+ // 2. Wait for the node to answer its chain API before bootstrapping against it.
55
+ const healthUrl = env.EVOLVER_INSTALL_HEALTH_URL?.trim() || LOCAL_CHAIN_URL + HEALTH_PATH;
56
+ const timeoutMs = Number(env.EVOLVER_INSTALL_TIMEOUT_MS ?? "120000");
57
+ const ok = await waitHealthy(healthUrl, timeoutMs);
58
+ if (!ok) {
59
+ const logs = spawnSync("docker", ["compose", "-f", composeFile, "logs", "--tail", "40", "node"], {
60
+ encoding: "utf8",
61
+ env: { ...process.env, ...env },
62
+ });
63
+ return {
64
+ exitCode: 1,
65
+ toStderr: true,
66
+ output: `install: node did not become healthy at ${healthUrl} within ${timeoutMs}ms.\nnode logs:\n${logs.stdout || logs.stderr || "(no logs)"}\n`,
67
+ };
68
+ }
69
+ // 3. Bootstrap the SEPARATE agent identity against the local node. `--target local` +
70
+ // EVOLVER_NODE=http://127.0.0.1:8440 makes the bootstrap derive
71
+ // EVOLVER_CONTENT_URL=http://127.0.0.1:8450 (the local node's content listener).
72
+ const nodeEnv = {
73
+ ...process.env,
74
+ ...env,
75
+ EVOLVER_NODE: env.EVOLVER_NODE ?? LOCAL_CHAIN_URL,
76
+ };
77
+ const boot = await runBootstrap(argv, nodeEnv);
78
+ return { exitCode: boot.exitCode, output: boot.output, toStderr: boot.toStderr };
79
+ }
80
+ /** Poll `url` until it answers 200 (or a healthy node's success), bounded by `timeoutMs`. */
81
+ async function waitHealthy(url, timeoutMs) {
82
+ const deadline = Date.now() + timeoutMs;
83
+ while (Date.now() < deadline) {
84
+ try {
85
+ const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
86
+ if (r.ok)
87
+ return true;
88
+ }
89
+ catch {
90
+ // not up yet — retry below
91
+ }
92
+ await new Promise((r) => setTimeout(r, 2000));
93
+ }
94
+ return false;
95
+ }
96
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcsherrylabs/evolver-mcp",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "MCP server over a configurable Evolver node — read the chain, sign a transfer, administer what you own, write to a channel, submit a signed transaction. Wraps @mcsherrylabs/evolver-client; implements no wire format of its own.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -12,7 +12,8 @@
12
12
  "evolver-mcp": "dist/cli.js"
13
13
  },
14
14
  "files": [
15
- "dist"
15
+ "dist",
16
+ "deploy"
16
17
  ],
17
18
  "publishConfig": {
18
19
  "access": "public"
@@ -41,8 +42,9 @@
41
42
  "check:group": "tsx test/group-checks.ts",
42
43
  "check:channel": "tsx test/channel-checks.ts",
43
44
  "check:bootstrap": "tsx test/bootstrap-checks.ts",
45
+ "check:install": "tsx test/install-checks.ts",
44
46
  "check:publish": "tsx test/publish-checks.ts",
45
- "test": "npm run build && npm run typecheck && npm run check:result && npm run check:config && npm run check:heights && npm run check:principal && npm run check:keys && npm run check:channelterms && npm run check:surface && npm run check:signing && npm run check:interval && npm run check:classify && npm run check:identity && npm run check:claim && npm run check:content && npm run check:inbox && npm run check:group && npm run check:channel && npm run check:bootstrap && npm run check:publish"
47
+ "test": "npm run build && npm run typecheck && npm run check:result && npm run check:config && npm run check:heights && npm run check:principal && npm run check:keys && npm run check:channelterms && npm run check:surface && npm run check:signing && npm run check:interval && npm run check:classify && npm run check:identity && npm run check:claim && npm run check:content && npm run check:inbox && npm run check:group && npm run check:channel && npm run check:bootstrap && npm run check:install && npm run check:publish"
46
48
  },
47
49
  "dependencies": {
48
50
  "@mcsherrylabs/evolver-client": "0.19.1",