@mcsherrylabs/pi-meerkat 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +85 -33
  2. package/dist/installer/committee-init.sh +117 -0
  3. package/dist/installer/committee.conf +102 -0
  4. package/dist/installer/node-join.sh +228 -0
  5. package/dist/installer/node.conf +235 -0
  6. package/dist/installer/runtime.json +1 -0
  7. package/dist/installer-runtime/dist/scripts/recover-owner.js +15 -0
  8. package/dist/installer-runtime/dist/scripts/subscriptions-init.js +35 -0
  9. package/dist/installer-runtime/dist/scripts/versions.mjs +86 -0
  10. package/dist/installer-runtime/dist/scripts/wait-for-chain.js +90 -0
  11. package/dist/installer-runtime/dist/src/announcements.js +448 -0
  12. package/dist/installer-runtime/dist/src/app.js +539 -0
  13. package/dist/installer-runtime/dist/src/chainfetch.js +87 -0
  14. package/dist/installer-runtime/dist/src/env.js +425 -0
  15. package/dist/installer-runtime/dist/src/registry/announce.js +77 -0
  16. package/dist/installer-runtime/dist/src/registry/announcement.js +141 -0
  17. package/dist/installer-runtime/dist/src/registry/dependencies.js +28 -0
  18. package/dist/installer-runtime/dist/src/registry/detail.js +87 -0
  19. package/dist/installer-runtime/dist/src/registry/drainer.js +131 -0
  20. package/dist/installer-runtime/dist/src/registry/http.js +194 -0
  21. package/dist/installer-runtime/dist/src/registry/index.js +52 -0
  22. package/dist/installer-runtime/dist/src/registry/lease.js +35 -0
  23. package/dist/installer-runtime/dist/src/registry/ownership.js +31 -0
  24. package/dist/installer-runtime/dist/src/registry/provision.js +180 -0
  25. package/dist/installer-runtime/dist/src/registry/store.js +242 -0
  26. package/dist/installer-runtime/dist/src/registry/sweep.js +9 -0
  27. package/dist/installer-runtime/dist/src/registry/worker-module.js +87 -0
  28. package/dist/installer-runtime/dist/src/runtime.js +164 -0
  29. package/dist/installer-runtime/dist/src/store.js +138 -0
  30. package/dist/installer-runtime/package.json +1 -0
  31. package/dist/installer.d.ts +224 -0
  32. package/dist/installer.d.ts.map +1 -0
  33. package/dist/installer.js +336 -0
  34. package/dist/installer.js.map +1 -0
  35. package/dist/mcp-cli.d.ts.map +1 -1
  36. package/dist/mcp-cli.js +31 -5
  37. package/dist/mcp-cli.js.map +1 -1
  38. package/dist/registry.d.ts.map +1 -1
  39. package/dist/registry.js +8 -4
  40. package/dist/registry.js.map +1 -1
  41. package/installer/Committee.Dockerfile +6 -0
  42. package/installer/Node.Dockerfile +6 -0
  43. package/installer/Registry.Dockerfile +13 -0
  44. package/installer/runtime-control.mjs +172 -0
  45. package/installer/session-files.mjs +16 -0
  46. package/package.json +5 -4
package/README.md CHANGED
@@ -5,28 +5,92 @@ Meerkat tools plus every tool dynamically discovered from the private
5
5
  `@mcsherrylabs/evolver-mcp@0.15.0` child. The released upstream baseline is 65 tools, so healthy
6
6
  discovery currently returns 72. The count is a comparison baseline, not a routing allowlist.
7
7
 
8
- **Version 0.7.0 introduces this facade.** Version 0.6.0 is the previous Pi/local-signing package
9
- and does **not** contain it. Use the explicit version below, not an unversioned `npx` command or
10
- an assumption about npm `latest`. Source/packed acceptance, publication, and verification of an
11
- installed published release are separate milestones.
8
+ **Version 0.8.0 adds the zero-required-parameter installer.** Version 0.7.0 introduced the
9
+ facade but required an existing node, registry, identity and key. Version 0.6.0 is the old
10
+ Pi/local-signing package. Always use an exact published version, not npm `latest`.
12
11
 
13
- ## Install from npm
12
+ ## Install the complete experience
14
13
 
15
- Run this in the same environment where Claude Code (or another MCP client) runs. Node 22 is
16
- the verified runtime. Installation downloads the package and its exact released dependency;
17
- it does not provision or start a node:
14
+ Prerequisites in the environment where your MCP harness runs:
15
+
16
+ - Node.js **22 or newer**, with `npm` and `npx` on `PATH`.
17
+ - A reachable Docker Engine and **Docker Compose v2** (`docker version`, `docker compose version`).
18
+ A normal local engine or a separately hosted Docker daemon/context both work. Existing Docker
19
+ authentication/TLS settings are respected; no new credentials are requested for public images.
20
+ - An MCP harness with stdio support. The installer prints registration JSON; it does not edit
21
+ the harness or silently replace an existing registration.
22
+
23
+ ### 1. Install
18
24
 
19
25
  ```bash
20
- npm install --prefix "$HOME/.local/share/meerkat-mcp/0.7.0" \
21
- --no-audit --no-fund @mcsherrylabs/pi-meerkat@0.7.0
22
- export PATH="$HOME/.local/share/meerkat-mcp/0.7.0/node_modules/.bin:$PATH"
26
+ npx --yes @mcsherrylabs/pi-meerkat@0.8.0 install
23
27
  ```
24
28
 
25
- The installed facade resolves and launches its actual dependency's `evolver-mcp` bin
26
- (`dist/cli.js`). It does not use a sibling source checkout, global executable, or unversioned
27
- runtime download. Installation under the home directory persists across rig recreation.
29
+ No identity, key, endpoint, registry, chain ID, channel, port, GID, path, environment variables
30
+ or paid model service is required. This creates an **independent private local chain** and
31
+ starts its owned consensus node, separate processing node and Meerkat registry/content/inbox services. It does **not** join the
32
+ shared devnet, claim from its faucet, inspect unrelated keys, or change another deployment.
33
+ The first install downloads public pinned artifacts and builds three wrapper images, so
34
+ allow time for Docker/npm downloads. Failed preflight/build/readiness exits nonzero with a reason.
35
+
36
+ ### 2. Register one server
37
+
38
+ Use the one `registration.mcpServers.meerkat` entry printed by installation. For Claude Code,
39
+ if you do not already have a registration named `meerkat`, the equivalent command is:
40
+
41
+ ```bash
42
+ claude mcp add --scope user --transport stdio meerkat -- \
43
+ npx --yes @mcsherrylabs/pi-meerkat@0.8.0 serve
44
+ ```
28
45
 
29
- ## Configure an existing node once
46
+ Do not remove or replace an unrelated `meerkat` registration. Choose a different harness
47
+ registration name for this entry if necessary; its underlying saved deployment remains the same.
48
+ Other MCP clients can add the printed stdio JSON entry directly. Do **not** register an Evolver
49
+ child separately. Restart the harness connection, inspect its tools, and begin with
50
+ `meerkat_summary`, `meerkat_services`, `meerkat_inbox` and `evolver_chain_height`.
51
+
52
+ `serve` finds the installer's saved state automatically; there is no `--config` or environment
53
+ ceremony. The public package launcher is pinned and survives npm-cache cleanup. Each client
54
+ gets its **own facade and exact Evolver child/session/store** inside the owned runtime container,
55
+ connected through Docker exec stdio. Node/content/registry HTTP stays on the private owned
56
+ Docker network, with **no host-published ports**, so a separate Docker daemon needs neither
57
+ host-path sharing nor a public read port.
58
+ Stdout carries MCP protocol only. Closing the connection or signalling the launcher awaits the
59
+ exact remote facade and its private child, not the shared registry or another client's session.
60
+
61
+ ### Resume and ownership
62
+
63
+ Run the **same install command** again to resume the same installation. Names, identity/key
64
+ references, internal service ports, daemon identity, real genesis/node binding and ownership are saved
65
+ privately under the operator's XDG state directory (or normal home state directory). Services
66
+ use separately owned Docker volumes for consensus data, processing-node data, public genesis,
67
+ registry/app state, subscriptions and announcer state. Consensus and consumer-paced processing
68
+ are separate roles required by the released node. The follower receives only the **public** chain
69
+ specification, never the consensus node's private volume. The released Evolver bootstrap alone
70
+ creates/loads announcer signing keys; the facade,
71
+ installer and output carry only references. No key bytes are copied to the harness.
72
+
73
+ Rerun does not initialize subscriptions again, regenerate genesis, rotate keys, change authority,
74
+ force-claim/unclaim, adopt another daemon's resources or recover an owner lock. A missing valued
75
+ volume, conflicting ownership, changed daemon/binding, interrupted initializer or stale owner lock
76
+ refuses explicitly and retains state for operator inspection. There is no automatic reset,
77
+ force recovery, Docker prune, broad restart or harness-registration mutation. `serve` never
78
+ creates or restarts services. An intentional **install** invocation may start its own stopped
79
+ services; unrelated services are never touched. Automatic cross-version migration is not provided:
80
+ resume with the saved pinned version.
81
+
82
+ The installer enables the complete registry/read/announce experience, not every optional upstream
83
+ capability. Generic Evolver subscription-control and other optional capability tools can remain
84
+ unconfigured and refuse clearly; no Docker socket or private node-control API is exposed to the
85
+ registry/facade. The real tool catalog baseline is 65 upstream + 7 local, **not a promise that each
86
+ optional capability is configured**.
87
+
88
+ ## Advanced: attach an existing node
89
+
90
+ If you already operate a node, registry and keys, you may install this exact package normally
91
+ and use `meerkat-mcp setup` instead. This is an alternative to the automatic installer, not a
92
+ prerequisite for it. Keep your installed package on a persistent user-owned PATH, not an
93
+ npm-cache-internal pathname.
30
94
 
31
95
  Setup records **references**, not key bytes. Select an existing node endpoint, separate content
32
96
  endpoint, identity, existing key file, registry read origin, explicit genesis/chain ID and node ID,
@@ -70,24 +134,12 @@ Pi adapter, or separately managed child is required. Direct launch uses the same
70
134
  meerkat-mcp --config "$MEERKAT_CONFIG"
71
135
  ```
72
136
 
73
- ### Register in Claude Code
74
-
75
- From the environment where Claude Code runs, register the installed facade with the same
76
- absolute config path used for setup:
77
-
78
- ```bash
79
- claude mcp add --scope user --transport stdio meerkat -- \
80
- "$(command -v node)" \
81
- "$HOME/.local/share/meerkat-mcp/0.7.0/node_modules/@mcsherrylabs/pi-meerkat/dist/mcp-cli.js" \
82
- --config "$MEERKAT_CONFIG"
83
- ```
84
-
85
- `--scope user` makes this operator registration available across your projects. Use
86
- `--scope local` instead to restrict the initial test to the current project. Start a fresh
87
- Claude Code session and run `/mcp` to inspect the connection and its tools. Do not register the
88
- private Evolver child separately. Begin with `meerkat_summary`, a small `meerkat_services`
89
- query, `meerkat_inbox`, and the upstream read-only `evolver_chain_height`; do not approve
90
- announcement/sign/submit calls until you deliberately choose to exercise a write.
137
+ For advanced attach, register the exact persistent installed entrypoint/config returned by
138
+ setup; do not point a client at an npm-cache-internal file. The automatic installer's pinned
139
+ `serve` registration above is the recommended route for a virgin harness. In Claude Code,
140
+ `--scope user` makes the operator registration available across projects; `--scope local`
141
+ limits it to the current project. Start a fresh session and use `/mcp` to inspect the connection.
142
+ Do not approve announcement/sign/submit calls until you deliberately choose to exercise a write.
91
143
 
92
144
  The stdio connection uses stdout exclusively for MCP messages. Diagnostics go to stderr. The
93
145
  operator-selected node/content/trust/identity/key/channel inputs are captured at facade startup;
@@ -0,0 +1,117 @@
1
+ #!/bin/sh
2
+ # node entrypoint: host keys + genesis on first boot, then exec the node.
3
+ #
4
+ # On FIRST boot it generates this node's key material ON THE HOST (never in the
5
+ # image or the repo), renders a one-node genesis committee from the derived public
6
+ # keys, and persists everything into the /data volume. On every subsequent boot it
7
+ # REUSES the same keys + genesis so the chain continues — the genesis committee must
8
+ # stay byte-stable or the chain identity diverges.
9
+ #
10
+ # Two secrets, handled identically (spec 101 — both are host-only secrets):
11
+ # - Ed25519 node-identity key -> ce.evolver.node.private-key (EVOLVER_PRIVATE_KEY)
12
+ # - BLS consensus key (IKM) -> ce.evolver.node.bls-secret (EVOLVER_BLS_SEED)
13
+ # Their public keys go into genesis (public-key / bls-public-key).
14
+ set -eu
15
+
16
+ # Deliberately NOT derived from the app name. This id is written into the genesis
17
+ # file on the volume at FIRST BOOT, so changing it on an existing chain would launch
18
+ # the node under a name absent from its own genesis. Keeping it neutral means
19
+ # `npm run rename` never has to touch this file and cannot leave a previous project's
20
+ # name behind — the template used to default to `tmplnode0`, so every app built from
21
+ # it had a node introducing itself by the TEMPLATE's name, in its logs and in its
22
+ # published deployment artifact.
23
+ NODE_ID="${NODE_ID:-node0}"
24
+ KEYS=/data/keys
25
+ GENESIS=/data/genesis.gen.conf
26
+ ED_KEY="$KEYS/ed25519.key" # base64 PKCS#8 Ed25519 private
27
+ BLS_KEY="$KEYS/bls.key" # base64 BLS IKM (>=32 bytes)
28
+
29
+ if [ -f "$ED_KEY" ] || [ -f "$BLS_KEY" ] || [ -f "$GENESIS" ]; then
30
+ [ -s "$ED_KEY" ] && [ -s "$BLS_KEY" ] && [ -s "$GENESIS" ] || {
31
+ echo '[node-init] incomplete owned keys/genesis; refusing regeneration' >&2; exit 1;
32
+ }
33
+ fi
34
+ if [ ! -f "$ED_KEY" ]; then
35
+ echo "[node-init] first boot: generating host key material for '$NODE_ID'"
36
+ mkdir -p "$KEYS"; chmod 700 "$KEYS"
37
+
38
+ # Ed25519 identity keypair — PKCS#8 private + X.509 public, base64 DER.
39
+ tmp="$(mktemp)"
40
+ openssl genpkey -algorithm ed25519 -outform DER -out "$tmp" 2>/dev/null
41
+ ED_PRIV="$(base64 -w0 < "$tmp")"
42
+ ED_PUB="$(openssl pkey -inform DER -in "$tmp" -pubout -outform DER 2>/dev/null | base64 -w0)"
43
+ rm -f "$tmp"
44
+
45
+ # BLS consensus keypair — random IKM + derived pubkey, via the evolver keygen.
46
+ BLS_OUT="$(java -cp "/opt/evolver/lib/*" ce.evolver.crypto.keys.BLSKeyGenerator)"
47
+ BLS_SECRET="$(echo "$BLS_OUT" | awk '/^bls-secret:/{print $2}')"
48
+ BLS_PUB="$(echo "$BLS_OUT" | awk '/^bls-public-key:/{print $2}')"
49
+
50
+ if [ -z "$ED_PRIV" ] || [ -z "$ED_PUB" ] || [ -z "$BLS_SECRET" ] || [ -z "$BLS_PUB" ]; then
51
+ echo "[node-init] FATAL: key generation produced empty output" >&2
52
+ exit 1
53
+ fi
54
+
55
+ # Persist secrets — host-only, mode 600.
56
+ printf '%s' "$ED_PRIV" > "$ED_KEY"; chmod 600 "$ED_KEY"
57
+ printf '%s' "$BLS_SECRET" > "$BLS_KEY"; chmod 600 "$BLS_KEY"
58
+
59
+ # Render the chain spec (089): committee PUBLIC keys + the chain-wide consensus
60
+ # parameters. This exact file is published verbatim to every follower / app node
61
+ # (102 follower-join: the genesis file IS the chain, byte-identical everywhere) and
62
+ # its canonical hash is pinned as the genesis block id (ce.evolver.genesis-file in
63
+ # node.conf). 103: the host is the ROUTABLE public address — followers dial the
64
+ # committee at exactly this host:port. Chain-wide params live HERE, not in node.conf,
65
+ # so joiners inherit them automatically — quiescence especially (FR-019: a joiner
66
+ # with different quiescence would time out the deliberately-lingering leader).
67
+ PUBLIC_HOST="${PUBLIC_HOST:-localhost}"
68
+ cat > "$GENESIS" <<EOF
69
+ ce.evolver.genesis.nodes = [
70
+ {
71
+ node-id = "$NODE_ID"
72
+ host = "$PUBLIC_HOST"
73
+ port = 7000
74
+ public-key = "$ED_PUB"
75
+ bls-public-key = "$BLS_PUB"
76
+ quorum-weight = 1000
77
+ }
78
+ ]
79
+
80
+ ce.evolver.consensus {
81
+ n-nodes = 1
82
+ block-time-ms = 600
83
+ round-timeout-ms = 1500
84
+ block-reward-tokens = 10000 # generous dev reward: the faucet (5000/identity) is funded from the FIRST blocks, even under quiescence
85
+ snapshot-interval-blocks = 0 # single-node: no snapshot cadence; joiners sync from block 0
86
+ identity-sponsorship-cost = 100
87
+
88
+ # Low idle disk-write: after 3 empty blocks the leader lingers on a 5-MINUTE heartbeat
89
+ # instead of the 600ms cadence (#31: idle-heartbeat-ms > block-time-ms activates quiescence);
90
+ # wake-on-work (node-local, always on) still proposes immediately on incoming txs.
91
+ # CHAIN-WIDE — identical on every node by construction.
92
+ quiescence {
93
+ idle-heartbeat-ms = 300000
94
+ idle-after-empty-rounds = 3
95
+ }
96
+ }
97
+ EOF
98
+ echo "[node-init] generated keys (secrets in $KEYS, mode 600) + chain spec (host=$PUBLIC_HOST)"
99
+ echo "[node-init] PUBLISH the chain spec (public keys + chain-wide config): docker compose cp node:/data/genesis.gen.conf ."
100
+ else
101
+ echo "[node-init] existing keys found in $KEYS — reusing (chain continues)"
102
+ fi
103
+
104
+ # Secrets reach the node via env (HOCON ${?...} substitution in node.conf).
105
+ EVOLVER_PRIVATE_KEY="$(cat "$ED_KEY")"; export EVOLVER_PRIVATE_KEY
106
+ EVOLVER_BLS_SEED="$(cat "$BLS_KEY")"; export EVOLVER_BLS_SEED
107
+ mkdir -p /data/chain
108
+
109
+ echo "[node-init] launching evolver node '$NODE_ID'"
110
+ if [ -f /public-genesis/genesis.conf ]; then
111
+ cmp -s "$GENESIS" /public-genesis/genesis.conf || { echo '[node-init] public genesis differs; refusing replacement' >&2; exit 1; }
112
+ else
113
+ cp "$GENESIS" /public-genesis/genesis.pending
114
+ chmod 640 /public-genesis/genesis.pending
115
+ mv /public-genesis/genesis.pending /public-genesis/genesis.conf
116
+ fi
117
+ exec /opt/evolver/bin/evolver -Dconfig.file=/config/node.conf "$NODE_ID"
@@ -0,0 +1,102 @@
1
+ # Single-node full-quorum, quiescent. The chain spec (committee with this
2
+ # node's generated public keys + ALL chain-wide consensus params, incl. quiescence) is
3
+ # rendered at first boot by node-init.sh into /data/genesis.gen.conf and included
4
+ # here — this file holds only NODE-LOCAL config. Secrets arrive via env (never in this
5
+ # file).
6
+ include file("/data/genesis.gen.conf")
7
+
8
+ # 089 chain-spec pin: the canonical hash of the genesis file IS the chain's genesis
9
+ # block id. Joiners set this to their verbatim copy — any drift refuses to boot.
10
+ ce.evolver.genesis-file = "/data/genesis.gen.conf"
11
+
12
+ ce.evolver.node {
13
+ private-key = ${?EVOLVER_PRIVATE_KEY} # Ed25519 PKCS#8 (host secret, from node-init.sh)
14
+ bls-secret = ${?EVOLVER_BLS_SEED} # BLS IKM (host secret, from node-init.sh)
15
+ }
16
+
17
+ evolver.peer-discovery {
18
+ # What this node ADVERTISES about ITSELF to peers — not somewhere it connects to.
19
+ # This chain is standalone (one node, its own genesis, no seeds) so nothing reads
20
+ # these; localhost is used because the alternative is advertising an address that
21
+ # is not this node. They named devnet.mcsherrylabs.com purely because this config
22
+ # was copied from that deployment. Joining a real chain means setting them to the
23
+ # ROUTABLE p2p transport (raw TCP mTLS :7000) and HTTPS API your peers can dial.
24
+ # 163 — `host:port`, no scheme: the transport is raw TCP + mTLS and never was a URL.
25
+ transport-address = "committee:7000"
26
+ # No `peer-url`: this chain is one node, so nothing fetches content from it, and the
27
+ # plain-HTTP :8440 above could not serve a peer anyway — a content fetch carries the
28
+ # caller's client certificate and needs the node's OWN TLS listener. Joining a real
29
+ # chain means setting `transport-address` to the routable :7000 and `peer-url` to this
30
+ # node's https listener (never a reverse proxy: it presents no client certificate).
31
+ gossip-interval-seconds = 60
32
+ }
33
+
34
+ # Chain-wide consensus params (n-nodes, block-time, quiescence, ...) come from the
35
+ # included chain spec — do NOT set them here, they must stay byte-identical on every
36
+ # node. Only node-local knobs below.
37
+ ce.evolver.consensus {
38
+ validate-chain-on-startup = false
39
+
40
+ # Reactivity (node-local, always on since #28): on incoming work the leader proposes
41
+ # IMMEDIATELY rather than waiting out the (5-minute) idle heartbeat — tx latency stays
42
+ # sub-second despite slow idle.
43
+ wake-on-work {
44
+ forward-watermark = 16
45
+ }
46
+ }
47
+
48
+ data.base = "/data/chain"
49
+ state.snapshots = "snapshots"
50
+ # PLAIN HTTP on 8440 for everything that TALKS to this node, deliberately. The HTTPS
51
+ # listener presents the node's IDENTITY certificate — self-signed, and valid for ~13
52
+ # MINUTES, because it is a peer authentication credential and not a web certificate. No
53
+ # browser can be made happy with that: an exception you click through expires before you
54
+ # finish reading the page. Tolerating it in code means NODE_TLS_REJECT_UNAUTHORIZED=0,
55
+ # which disables verification PROCESS-WIDE for every host — a bad habit to ship in a
56
+ # template.
57
+ #
58
+ # TLS on 8441 for PEERS, because the content cache needs somewhere mTLS to live: its peer
59
+ # route identifies callers by client certificate and the node refuses to start content
60
+ # without a TLS listener. Before evolver v0.16.0 those were the same listener, so
61
+ # "hold documents" and "put every client on an unpinnable certificate" were one decision.
62
+ # They are not, and this is what that separation looks like in a deployment.
63
+ http.port = 8440
64
+ # ⚠️ REQUIRED since evolver #131: the plain API listener now defaults to 127.0.0.1. The app
65
+ # container reaches this node over the CONTAINER NETWORK, which is not loopback from the
66
+ # node's point of view — without this line the app gets connection refused and the node
67
+ # logs nothing unusual. "0.0.0.0" is what this deployment effectively had before.
68
+ http.bind-host = "0.0.0.0"
69
+ https.port = 8441
70
+ evolver.grpc.port = 9000
71
+
72
+ # 139 — CONTENT, so a detail document announced on this chain can actually be fetched.
73
+ #
74
+ # `bind-host` is what lets a SIBLING CONTAINER reach the publish/read route: it is
75
+ # authorized by what can reach it, and the default (loopback) means "inside this node's
76
+ # container and nowhere else". 0.0.0.0 here says: this compose project's network is the
77
+ # boundary. It holds because the port below is published to 127.0.0.1 only.
78
+ ce.evolver.content {
79
+ enabled = true
80
+ http-port = 8450
81
+ bind-host = "0.0.0.0"
82
+ }
83
+
84
+ chainDb {
85
+ datasource {
86
+ driver = "org.hsqldb.jdbc.JDBCDriver"
87
+ user = "sa"
88
+ pass = ""
89
+ maxPoolSize = 5
90
+ connectionProperties = ""
91
+ cachePrepStmts = true
92
+ prepStmtCacheSize = 250
93
+ prepStmtCacheSqlLimit = 2048
94
+ connectionTimeout = 5000
95
+ useServerPrepStmts = false
96
+ transactionIsolationLevel = "TRANSACTION_READ_COMMITTED"
97
+ }
98
+ viewCachesSize = 50
99
+ useShutdownHook = false
100
+ freeBlobsEarly = false
101
+ }
102
+ ce.evolver.execution.enabled = false
@@ -0,0 +1,228 @@
1
+ #!/bin/sh
2
+ # Claim-then-boot (evolver feature 102) for an explicitly managed follower.
3
+ # Static config is baked into the candidate image. The separately initialized subscription
4
+ # directory is mounted read-only; the installer provisions/persists APP_NODE_ID before boot.
5
+ # Ordinary entry never initializes subscription preferences or adopts existing valued state.
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
+ # Same read-only guard on normal boot and the owned-fixture preflight. It NEVER initializes,
14
+ # rewrites or merges a declaration; canonical fragment validation belongs to the shared SDK.
15
+ require_subscriptions() {
16
+ if [ ! -r "$1/channels.conf" ] ||
17
+ [ "$(cat "$1/channels.conf.initialized" 2>/dev/null || true)" != "evolver-subscriptions-v1" ]; then
18
+ echo "[node-join] FATAL: initialized subscription declaration missing; refusing to reseed"
19
+ return 1
20
+ fi
21
+ }
22
+ if [ "${1:-}" = "--check-subscriptions" ]; then
23
+ : "${2:?owned subscription directory required}"
24
+ require_subscriptions "$2"
25
+ exit $?
26
+ fi
27
+ require_subscriptions /subscriptions
28
+
29
+ : "${DEVNET_API:?DEVNET_API required (the faucet base url, e.g. https://devnet.mcsherrylabs.com)}"
30
+
31
+ # ⚠️ CHECKED HERE, not left to HOCON. `preferred-url` is a REQUIRED substitution in the
32
+ # node config, so an unset value fails with
33
+ # ConfigException$UnresolvedSubstitution: Could not resolve substitution to a value
34
+ # and a stack trace that says nothing about what an operator must do. Required is the
35
+ # right choice — an unroutable default is what made this node dial its own loopback and
36
+ # stop forwarding submits — but the refusal has to be readable.
37
+ # NOTE: meerkat's version of this script FATALs here unless EVOLVER_TRANSPORT_ADDRESS is set. That is
38
+ # an ANNOUNCER's requirement — its channel has a DA group, so other nodes must be able to dial it
39
+ # to flood bodies to it. A reader has no such need and must advertise nothing, so the guard is
40
+ # deliberately absent rather than satisfied with a made-up value.
41
+
42
+
43
+ KEYS=/data/keys
44
+ CLAIM="$KEYS/claim.env"
45
+ ID_FILE="$KEYS/node-id"
46
+ mkdir -p "$KEYS"
47
+ chmod 700 "$KEYS"
48
+
49
+ # The node id is chosen ONCE and persisted, because it is an on-chain name: re-deriving
50
+ # it on a later boot would claim a SECOND identity and abandon the first.
51
+ if [ -f "$ID_FILE" ]; then
52
+ NODE_ID="$(cat "$ID_FILE")"
53
+ echo "[node-join] reusing node id '$NODE_ID' (chain state continues)"
54
+ else
55
+ if [ -n "${APP_NODE_ID:-}" ]; then
56
+ NODE_ID="$APP_NODE_ID"
57
+ else
58
+ # Identity names are ^[A-Za-z0-9]{1,50}$ — no hyphens, dots or underscores — so the
59
+ # app name is stripped to what a node id may contain (the same rule as
60
+ # scripts/versions.mjs identityPrefix), and a random suffix keeps a shared chain
61
+ # collision-free: a fixed name would come back `exists` with keys nobody holds.
62
+ BASE="$(printf '%s' "${APP_NAME:-app}" | tr -cd '[:alnum:]' | cut -c1-40)"
63
+ [ -n "$BASE" ] || BASE=app
64
+ NODE_ID="${BASE}node$(tr -cd '0-9' < /dev/urandom | head -c 6)"
65
+ fi
66
+ printf '%s' "$NODE_ID" > "$ID_FILE"
67
+ echo "[node-join] node id '$NODE_ID' chosen and persisted"
68
+ fi
69
+
70
+ : "${APP_NODE_ID:?managed deployment requires its saved explicit node id}"
71
+ if [ "$NODE_ID" != "$APP_NODE_ID" ]; then
72
+ echo "[node-join] FATAL: saved node id differs from managed binding; refusing to claim or boot"
73
+ exit 1
74
+ fi
75
+
76
+ if [ ! -f "$CLAIM" ]; then
77
+ # A CLAIM WHOSE CONFIRMATION TIMED OUT MUST NOT BE RE-SENT BLIND.
78
+ #
79
+ # Observed on the live devnet: the faucet answered
80
+ # `504 {"error":"confirmation_timeout","message":"Transaction submitted but block
81
+ # confirmation timed out"}` — submitted, outcome UNKNOWN — the container exited, and
82
+ # the next boot found the id file but no claim file and claimed AGAIN. That is the one
83
+ # move that cannot be undone: the client persists its keypair only on success, so the
84
+ # first attempt's keys are gone, and if that attempt committed the name now belongs to
85
+ # keys nobody holds. The retry then either takes a name with a fresh keypair (and the
86
+ # first identity is abandoned) or comes back `exists`, after which the node fails every
87
+ # peer handshake with `Signature does not match` — the 103 failure verbatim.
88
+ #
89
+ # So the outcome is settled by READING, exactly as the app settles a submit: if the
90
+ # name is on chain and we hold no keys for it, stop and say so. The name cannot be
91
+ # reclaimed, so this is terminal by nature and the operator needs a different one.
92
+ # ASK, AND DISTINGUISH "NO" FROM "COULD NOT ASK".
93
+ #
94
+ # `wget | grep -q '"balance"'` is false for BOTH "the name is free" and "the faucet
95
+ # is unreachable", and on this path the second must not be read as the first: the
96
+ # whole point is to avoid claiming a name that may already be bound to keys nobody
97
+ # holds. So the identity route is asked with the exit status kept, and only a body
98
+ # that says `not_found` counts as free.
99
+ # 0 with "balance" -> taken
100
+ # 0 with "not_found" -> free
101
+ # anything else -> could not tell, which is not a licence to claim
102
+ # ⚠️ THE STATUS LINE, NOT THE EXIT CODE. `/api/identity/<name>` answers 404 for a name
103
+ # that is FREE — the commonest case — and `wget -qO-` reports any 404 as exit 8 with no
104
+ # body, so keying on the exit code classified "free" as "cannot ask" and refused to
105
+ # claim anything at all. Caught by the local rig, where every name is free.
106
+ #
107
+ # `-S` prints the response line even for an error status (GNU wget 1.25 in this image),
108
+ # so the three answers are distinguishable:
109
+ # 200 + "balance" -> taken
110
+ # 404 -> free
111
+ # anything else -> could not ask, which is NOT a licence to claim
112
+ id_state() {
113
+ # `|| true` IS LOAD-BEARING, and it is the second layer of the same trap. Keying on
114
+ # the status line instead of the exit code is necessary but not sufficient: under
115
+ # `set -e` the ASSIGNMENT inherits wget's exit status, so a 404 killed this function
116
+ # before the case could run — the caller saw an empty answer and the script exited 8.
117
+ _resp="$(wget --timeout=5 --tries=1 -S -qO- "$DEVNET_API/api/identity/$1" 2>&1)" || true
118
+ case "$_resp" in
119
+ *"200 OK"*'"balance"'*) echo taken ;;
120
+ *"200 OK"*) echo taken ;;
121
+ *"404"*) echo free ;;
122
+ *) echo unreachable ;;
123
+ esac
124
+ }
125
+ state="$(id_state "$NODE_ID")"
126
+ if [ "$state" = unreachable ]; then
127
+ echo "[node-join] FATAL: cannot reach $DEVNET_API to ask whether '$NODE_ID' is already"
128
+ echo "[node-join] claimed. Refusing to claim blind: if that name is already bound to keys"
129
+ echo "[node-join] this deployment does not hold, claiming it again destroys the name."
130
+ exit 1
131
+ fi
132
+ if [ "$state" = taken ]; then
133
+ echo "[node-join] FATAL: identity '$NODE_ID' already exists on chain, and this"
134
+ echo "[node-join] deployment holds no keys for it ($CLAIM is absent). A previous"
135
+ echo "[node-join] claim committed after its confirmation timed out, and its keys"
136
+ echo "[node-join] were never persisted — nothing here can authenticate as that name."
137
+ echo "[node-join] Set APP_NODE_ID to an unused name (an identity name cannot be reused)."
138
+ exit 1
139
+ fi
140
+ echo "[node-join] first boot: claiming identity '$NODE_ID' via $DEVNET_API"
141
+ java -cp "/opt/evolver/lib/*" ce.evolver.tools.IdentityClaimClient \
142
+ --target="$DEVNET_API" --node-id="$NODE_ID" --out="$CLAIM" \
143
+ --timeout="${CLAIM_TIMEOUT:-180}" || CLAIM_RC=$?
144
+ if [ ! -f "$CLAIM" ]; then
145
+ # OUR attempt's outcome is now the unknown one, and asking ONCE is not enough: a
146
+ # `504 confirmation_timeout` means the transaction was submitted, and it can commit
147
+ # minutes later (its TTL is +50 rounds, and on a quiescent chain rounds are slow).
148
+ # Asking immediately and printing "the name is still free" was wrong while the claim
149
+ # was in flight — and with `restart: unless-stopped` the next boot would claim the
150
+ # same name AGAIN with fresh keys, which is how a name gets bound to keys nobody
151
+ # holds. So watch it for a bounded window before saying anything.
152
+ echo "[node-join] the claim for '$NODE_ID' did not confirm (exit ${CLAIM_RC:-?}) —"
153
+ echo "[node-join] outcome UNKNOWN. Watching for up to ${CLAIM_WATCH_SECS:-300}s to see"
154
+ echo "[node-join] whether it commits; nothing is resent."
155
+ _waited=0
156
+ _final=free
157
+ while [ "$_waited" -lt "${CLAIM_WATCH_SECS:-300}" ]; do
158
+ _s="$(id_state "$NODE_ID")"
159
+ if [ "$_s" = taken ]; then _final=taken; break; fi
160
+ sleep 10
161
+ _waited=$((_waited + 10))
162
+ done
163
+ if [ "$_final" = taken ]; then
164
+ echo "[node-join] FATAL: the claim for '$NODE_ID' COMMITTED after its confirmation"
165
+ echo "[node-join] timed out, and its keys were never persisted (evolver#86)."
166
+ echo "[node-join] Set APP_NODE_ID to an unused name and start again — an identity"
167
+ echo "[node-join] name cannot be reused."
168
+ else
169
+ echo "[node-join] '$NODE_ID' is still not on chain after ${_waited}s, so the claim"
170
+ echo "[node-join] did not commit and the name remains free; a restart may retry it."
171
+ fi
172
+ exit 1
173
+ fi
174
+ chmod 600 "$CLAIM"
175
+ echo "[node-join] claim committed — keys persisted (mode 600)"
176
+ fi
177
+
178
+ # EVOLVER_PRIVATE_KEY / EVOLVER_NODE_PUBLIC_KEY / EVOLVER_BLS_SEED
179
+ set -a
180
+ . "$CLAIM"
181
+ set +a
182
+
183
+ mkdir -p /data/chain
184
+ # Config-only directory is mounted read-only into this node. Only the shared SDK controller
185
+ # writes it. Ordinary boot NEVER merges environment/module defaults or initializes a missing file.
186
+ # SDK initialization is an explicit fresh-resource command; its marker survives an empty set.
187
+ # Already required before any identity/claim effects at entry.
188
+ # Keep keys/chain private: no agent/container writes this node's database or fragment.
189
+ chmod 700 /data/chain
190
+
191
+ # ── WHICH PEERS THIS NODE DIALS ───────────────────────────────────────────
192
+ #
193
+ # ONE list rendered as BOTH keys, because they are one decision: `preferred-peers` says
194
+ # who, `allowed-outbound` says how many, and the pin is strict only while the two agree
195
+ # (node.conf says why). Computed here rather than written in node.conf for the same
196
+ # reason the channel list is — so the two halves cannot drift apart.
197
+ #
198
+ # The upstream is always dialled. EVOLVER_DA_PEERS adds the DA members this deployment
199
+ # must reach to PULL bodies for the channels declared above; without a slot for them,
200
+ # `ChannelOverlay.peers()` is empty for those channels and nothing is ever fetched.
201
+ _peers="${EVOLVER_UPSTREAM_NODE_ID}"
202
+ # EVOLVER_DA_PEERS, comma-separated, deduplicated against the upstream. The comment above has
203
+ # promised this since the file was written and the code never did it — so a deployment that set
204
+ # it got silence, and the only way to pin a second peer was to smuggle a comma into
205
+ # EVOLVER_UPSTREAM_NODE_ID, which works here only by accident of raw substitution and breaks the
206
+ # dedupe and the count in the sibling stacks that DO implement this. Same shape as
207
+ # evolver-llm/docker-compose.devnet.yml.
208
+ for _p in $(printf '%s' "${EVOLVER_DA_PEERS:-}" | tr ',' ' '); do
209
+ if [ -n "$_p" ] && [ "$_p" != "${EVOLVER_UPSTREAM_NODE_ID}" ]; then
210
+ _peers="$_peers, $_p"
211
+ fi
212
+ done
213
+ _outbound="${EVOLVER_ALLOWED_OUTBOUND:-16}"
214
+ echo "[node-join] pinning ${_peers} as the seed; allowing ${_outbound} outbound so discovery can happen"
215
+ {
216
+ # The seed is PINNED so there is always a way back to the chain. `allowed-outbound` is
217
+ # deliberately LARGER than that one entry: the pin is strict only while the two are equal, and a
218
+ # strict pin would stop this node dialling anything it discovers. Once it has peers it learns its
219
+ # own distance to the committee and forwards transactions along that path like any other node.
220
+ printf 'evolver.peer-discovery.preferred-peers = [ %s ]
221
+ ' "$_peers"
222
+ printf 'evolver.peer-discovery.allowed-outbound = %s
223
+ ' "$_outbound"
224
+ } > /data/chain/peers.conf
225
+
226
+ echo "[node-join] launching follower '$NODE_ID' — syncing devnet from block 0"
227
+ exec /opt/evolver/bin/evolver -Dconfig.file=/config/node.conf "$NODE_ID"
228
+