@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.
- package/README.md +85 -33
- package/dist/installer/committee-init.sh +117 -0
- package/dist/installer/committee.conf +102 -0
- package/dist/installer/node-join.sh +228 -0
- package/dist/installer/node.conf +235 -0
- package/dist/installer/runtime.json +1 -0
- package/dist/installer-runtime/dist/scripts/recover-owner.js +15 -0
- package/dist/installer-runtime/dist/scripts/subscriptions-init.js +35 -0
- package/dist/installer-runtime/dist/scripts/versions.mjs +86 -0
- package/dist/installer-runtime/dist/scripts/wait-for-chain.js +90 -0
- package/dist/installer-runtime/dist/src/announcements.js +448 -0
- package/dist/installer-runtime/dist/src/app.js +539 -0
- package/dist/installer-runtime/dist/src/chainfetch.js +87 -0
- package/dist/installer-runtime/dist/src/env.js +425 -0
- package/dist/installer-runtime/dist/src/registry/announce.js +77 -0
- package/dist/installer-runtime/dist/src/registry/announcement.js +141 -0
- package/dist/installer-runtime/dist/src/registry/dependencies.js +28 -0
- package/dist/installer-runtime/dist/src/registry/detail.js +87 -0
- package/dist/installer-runtime/dist/src/registry/drainer.js +131 -0
- package/dist/installer-runtime/dist/src/registry/http.js +194 -0
- package/dist/installer-runtime/dist/src/registry/index.js +52 -0
- package/dist/installer-runtime/dist/src/registry/lease.js +35 -0
- package/dist/installer-runtime/dist/src/registry/ownership.js +31 -0
- package/dist/installer-runtime/dist/src/registry/provision.js +180 -0
- package/dist/installer-runtime/dist/src/registry/store.js +242 -0
- package/dist/installer-runtime/dist/src/registry/sweep.js +9 -0
- package/dist/installer-runtime/dist/src/registry/worker-module.js +87 -0
- package/dist/installer-runtime/dist/src/runtime.js +164 -0
- package/dist/installer-runtime/dist/src/store.js +138 -0
- package/dist/installer-runtime/package.json +1 -0
- package/dist/installer.d.ts +224 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +336 -0
- package/dist/installer.js.map +1 -0
- package/dist/mcp-cli.d.ts.map +1 -1
- package/dist/mcp-cli.js +31 -5
- package/dist/mcp-cli.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -4
- package/dist/registry.js.map +1 -1
- package/installer/Committee.Dockerfile +6 -0
- package/installer/Node.Dockerfile +6 -0
- package/installer/Registry.Dockerfile +13 -0
- package/installer/runtime-control.mjs +172 -0
- package/installer/session-files.mjs +16 -0
- package/package.json +5 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Fixed-target installer helper, not an MCP tool or arbitrary URL/shell executor.
|
|
2
|
+
import { mkdir, readFile, writeFile, unlink, lstat } from 'node:fs/promises';
|
|
3
|
+
import { publishSessionPid } from './session-files.mjs';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { loadFacadeConfig, saveFacadeConfig } from '../dist/mcp-config.js';
|
|
7
|
+
const [mode, argument] = process.argv.slice(2);
|
|
8
|
+
const TIMEOUT = 180_000;
|
|
9
|
+
const agentDir = '/meerkat/agent';
|
|
10
|
+
const configPath = join(agentDir, 'facade.json');
|
|
11
|
+
async function json(path, headers) {
|
|
12
|
+
const response = await fetch(path, { headers: { Accept: 'application/json', ...headers }, signal: AbortSignal.timeout(5000) });
|
|
13
|
+
if (!response.ok) throw new Error(`read ${new URL(path).pathname}: HTTP ${response.status}`);
|
|
14
|
+
return response.json();
|
|
15
|
+
}
|
|
16
|
+
async function until(description, check) {
|
|
17
|
+
const end = Date.now() + TIMEOUT;
|
|
18
|
+
let reason = 'not observed';
|
|
19
|
+
while (Date.now() < end) {
|
|
20
|
+
try { return await check(); } catch (e) { if (e?.permanent) throw e; reason = e instanceof Error ? e.message : 'read failed'; }
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`${description} not ready within ${TIMEOUT / 1000}s: ${reason}; state retained, nothing reset`);
|
|
24
|
+
}
|
|
25
|
+
async function child(command, args, env) {
|
|
26
|
+
await new Promise((resolve, reject) => {
|
|
27
|
+
const proc = spawn(command, args, { env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
28
|
+
let reason = '';
|
|
29
|
+
proc.stderr.on('data', data => { reason = (reason + String(data)).slice(-8192); });
|
|
30
|
+
const timer = setTimeout(() => proc.kill('SIGTERM'), TIMEOUT);
|
|
31
|
+
const hard = setTimeout(() => proc.kill('SIGKILL'), TIMEOUT + 5000);
|
|
32
|
+
proc.once('error', reject);
|
|
33
|
+
proc.once('close', code => { clearTimeout(timer); clearTimeout(hard);
|
|
34
|
+
code === 0 ? resolve() : reject(new Error(`released Evolver bootstrap failed: ${reason}`)); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function parameters() {
|
|
38
|
+
const args = JSON.parse(argument);
|
|
39
|
+
if (!/^[A-Za-z0-9]{1,50}$/.test(args.nodeId) || !/^[A-Za-z0-9]{1,50}$/.test(args.committeeId) || !/^[A-Za-z0-9]{1,50}$/.test(args.identity) ||
|
|
40
|
+
!/^meerkat\.[0-9a-f]{24}\.announcements$/.test(args.channel)) throw new Error('invalid saved deployment parameters');
|
|
41
|
+
if (args.binding && (args.binding.nodeId !== args.nodeId || !/^[0-9a-f]{64}$/.test(args.binding.chainId))) throw new Error('invalid saved binding');
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
const sessions = join(agentDir, 'sessions');
|
|
45
|
+
function sessionPath(token) {
|
|
46
|
+
if (!/^[0-9a-f]{32}$/.test(token)) throw new Error('invalid owned session token');
|
|
47
|
+
return join(sessions, `${token}.json`);
|
|
48
|
+
}
|
|
49
|
+
async function stopSession(token) {
|
|
50
|
+
const path = sessionPath(token);
|
|
51
|
+
let saved;
|
|
52
|
+
try { saved = JSON.parse(await readFile(path, 'utf8')); }
|
|
53
|
+
catch (e) { if (e.code === 'ENOENT') return; throw e; }
|
|
54
|
+
if (!Number.isSafeInteger(saved.pid) || (saved.pid !== 0 && saved.pid < 2)) throw new Error('invalid owned session process');
|
|
55
|
+
if (saved.pid === 0) {
|
|
56
|
+
// The host allocated this token before starting exec; cancellation wins the startup race.
|
|
57
|
+
await writeFile(`${path}.cancel`, '', { flag: 'wx', mode: 0o600 }).catch(e => { if (e.code !== 'EEXIST') throw e; });
|
|
58
|
+
} else {
|
|
59
|
+
let cmd;
|
|
60
|
+
try { cmd = (await readFile(`/proc/${saved.pid}/cmdline`, 'utf8')).split('\0'); }
|
|
61
|
+
catch (e) {
|
|
62
|
+
if (e.code === 'ENOENT') {
|
|
63
|
+
try { await lstat(path); } catch (gone) { if (gone.code === 'ENOENT') return; throw gone; }
|
|
64
|
+
throw new Error('owned session ended without cleanup; no PID reuse or lock recovery');
|
|
65
|
+
}
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
if (!cmd.includes('serve-session') || !cmd.includes(token)) throw new Error('owned session PID no longer matches; refusing signal');
|
|
69
|
+
process.kill(saved.pid, 'SIGTERM');
|
|
70
|
+
}
|
|
71
|
+
const end = Date.now() + 15_000;
|
|
72
|
+
while (Date.now() < end) {
|
|
73
|
+
try {
|
|
74
|
+
await lstat(path);
|
|
75
|
+
if (saved.pid === 0 && JSON.parse(await readFile(path, 'utf8')).pid > 0) { await stopSession(token); return; }
|
|
76
|
+
} catch (e) { if (e.code === 'ENOENT') return; throw e; }
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
78
|
+
}
|
|
79
|
+
throw new Error('owned facade did not finish child/socket cleanup within 15s');
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
if (mode === 'prepare-session') {
|
|
83
|
+
await mkdir(sessions, { recursive: true, mode: 0o700 });
|
|
84
|
+
await writeFile(sessionPath(argument), JSON.stringify({ pid: 0 }), { flag: 'wx', mode: 0o600 });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (mode === 'discard-session') {
|
|
88
|
+
const path = sessionPath(argument);
|
|
89
|
+
const saved = JSON.parse(await readFile(path, 'utf8'));
|
|
90
|
+
if (saved.pid !== 0) throw new Error('cannot discard a running owned session');
|
|
91
|
+
await unlink(path);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (mode === 'serve-session') {
|
|
95
|
+
const path = sessionPath(argument);
|
|
96
|
+
const saved = JSON.parse(await readFile(path, 'utf8'));
|
|
97
|
+
if (saved.pid !== 0) throw new Error('owned session token is already running');
|
|
98
|
+
// Import/configuration has a startup interval before the facade installs its handlers.
|
|
99
|
+
// Install guards BEFORE publishing the PID; publish atomically so a concurrent stop never
|
|
100
|
+
// sees a truncated/partial descriptor or a live PID with no cleanup handler.
|
|
101
|
+
let cancelled = false;
|
|
102
|
+
const earlyStop = () => { cancelled = true; };
|
|
103
|
+
process.once('SIGTERM', earlyStop); process.once('SIGINT', earlyStop);
|
|
104
|
+
try {
|
|
105
|
+
await publishSessionPid(path, process.pid);
|
|
106
|
+
try { await lstat(`${path}.cancel`); cancelled = true; } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
107
|
+
if (cancelled) return;
|
|
108
|
+
process.argv = [process.execPath, new URL('../dist/mcp-cli.js', import.meta.url).pathname, '--config', configPath];
|
|
109
|
+
// The CLI's existing shutdown path handles EOF/signals and awaits its exact private child.
|
|
110
|
+
const facade = import('../dist/mcp-cli.js');
|
|
111
|
+
const abort = setInterval(() => { if (cancelled && process.listenerCount('SIGTERM') > 0) process.emit('SIGTERM'); }, 10);
|
|
112
|
+
try { await facade; } finally { clearInterval(abort); }
|
|
113
|
+
} finally {
|
|
114
|
+
process.off('SIGTERM', earlyStop); process.off('SIGINT', earlyStop);
|
|
115
|
+
await unlink(path);
|
|
116
|
+
await unlink(`${path}.cancel`).catch(e => { if (e.code !== 'ENOENT') throw e; });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (mode === 'stop-session') { await stopSession(argument); return; }
|
|
121
|
+
const args = parameters();
|
|
122
|
+
if (mode === 'committee-ready') {
|
|
123
|
+
await until('owned local consensus committee', async () => {
|
|
124
|
+
const status = await json('http://committee:8440/status');
|
|
125
|
+
if (status.nodeId && status.nodeId !== args.committeeId) throw Object.assign(new Error('owned committee identity differs'), { permanent: true });
|
|
126
|
+
if (status.nodeId !== args.committeeId || !Number.isInteger(status.blockHeight) || status.blockHeight < 2 || status.selfQuorumIndex !== 0 || status.appliedRound !== null)
|
|
127
|
+
throw new Error('committee has not committed/funded its local chain with ordinary apply');
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (mode === 'node-binding') {
|
|
132
|
+
const binding = await until('owned processing node', async () => {
|
|
133
|
+
const value = await json('http://node:8440/api/channel/processor-state');
|
|
134
|
+
if (!value.processing || !/^[0-9a-f]{64}$/.test(value.binding?.chainId)) throw new Error('processing/genesis observation not ready');
|
|
135
|
+
if (value.binding.nodeId !== args.nodeId) throw Object.assign(new Error('owned node identity differs; refusing re-pairing'), { permanent: true });
|
|
136
|
+
if (args.binding && args.binding.chainId !== value.binding.chainId) throw Object.assign(new Error('saved genesis differs; refusing re-pairing'), { permanent: true });
|
|
137
|
+
return value.binding;
|
|
138
|
+
});
|
|
139
|
+
process.stdout.write(JSON.stringify(binding) + '\n');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!args.binding) throw new Error('missing saved node binding');
|
|
143
|
+
const headers = { 'x-evolver-chain-id': args.binding.chainId, 'x-evolver-node-id': args.binding.nodeId };
|
|
144
|
+
if (mode === 'ready') {
|
|
145
|
+
await until('Meerkat registry/channel/inbox', async () => {
|
|
146
|
+
const summary = await json('http://127.0.0.1:8090/summary', headers);
|
|
147
|
+
await json('http://127.0.0.1:8090/inbox', headers);
|
|
148
|
+
if (summary.channel !== args.channel) throw new Error('registry channel differs');
|
|
149
|
+
const terms = await json(`http://node:8440/api/explorer/channels/${encodeURIComponent(args.channel)}`);
|
|
150
|
+
if (!Array.isArray(terms.owners) || !terms.owners.length || terms.writePolicy !== 'open' || terms.da) throw new Error('registry channel not provisioned with open writes/no DA');
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (mode === 'bootstrap') {
|
|
155
|
+
const { resolveUpstreamBin } = await import('../dist/mcp-upstream.js');
|
|
156
|
+
const cli = await resolveUpstreamBin();
|
|
157
|
+
await child(process.execPath, [cli, '--bootstrap', '--target', 'local', '--identity', args.identity], {
|
|
158
|
+
PATH: process.env.PATH, HOME: agentDir, EVOLVER_NODE: 'http://node:8440', EVOLVER_FAUCET_URL: 'http://committee:8440',
|
|
159
|
+
EVOLVER_CONTENT_URL: 'http://node:8450', EVOLVER_MCP_STATE_DIR: agentDir, EVOLVER_BOOTSTRAP_CWD: agentDir,
|
|
160
|
+
});
|
|
161
|
+
const config = { version: 1, url: 'http://node:8440', identity: args.identity, tls: { insecure: false },
|
|
162
|
+
meerkat: { contentUrl: 'http://node:8450', keyPath: join(agentDir, `${args.identity}.key`), keyType: 'bls',
|
|
163
|
+
registryUrl: 'http://127.0.0.1:8090', chainId: args.binding.chainId, nodeId: args.nodeId, channel: args.channel } };
|
|
164
|
+
let existing;
|
|
165
|
+
try { existing = await loadFacadeConfig(configPath); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
166
|
+
if (existing) { if (JSON.stringify(existing) !== JSON.stringify(config)) throw new Error('unrelated saved facade authority; refusing replacement'); }
|
|
167
|
+
else await saveFacadeConfig(configPath, config);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
throw new Error('unknown installer runtime operation');
|
|
171
|
+
}
|
|
172
|
+
await main().catch(error => { console.error(error instanceof Error ? error.message : 'installer runtime failed'); process.exitCode = 1; });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A remote session PID must become visible all at once; stop-session may read concurrently.
|
|
2
|
+
import { open, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
export async function publishSessionPid(path, pid, prepared = async () => {}) {
|
|
4
|
+
const temporary = `${path}.${pid}.tmp`;
|
|
5
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
6
|
+
let renamed = false;
|
|
7
|
+
try {
|
|
8
|
+
try { await handle.writeFile(JSON.stringify({ pid })); await handle.sync(); }
|
|
9
|
+
finally { await handle.close(); }
|
|
10
|
+
await prepared();
|
|
11
|
+
await rename(temporary, path);
|
|
12
|
+
renamed = true;
|
|
13
|
+
} finally {
|
|
14
|
+
if (!renamed) await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcsherrylabs/pi-meerkat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Meerkat stdio MCP facade and registry tools, with a private pinned Evolver MCP child.
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Meerkat zero-parameter local installer, stdio MCP facade and registry tools, with a private pinned Evolver MCP child. Preserves the announcement contract and existing Pi extension.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"meerkat-mcp": "./dist/mcp-cli.js"
|
|
7
7
|
},
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/bundle-installer.mjs",
|
|
21
21
|
"prepack": "npm run build",
|
|
22
22
|
"clean": "rm -rf dist",
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"check:surface": "tsx test/surface-checks.ts",
|
|
26
26
|
"test": "npm run build && npm run typecheck && npm run check:contract && npm run check:surface && npm run check:inbox && npm run check:mcp:unit && npm run check:plan-two:client",
|
|
27
27
|
"check:inbox": "tsx test/inbox-tool-checks.ts",
|
|
28
|
-
"check:mcp:unit": "tsx test/mcp-announce-checks.ts && tsx test/mcp-facade-unit-checks.ts",
|
|
28
|
+
"check:mcp:unit": "tsx test/mcp-announce-checks.ts && tsx test/mcp-facade-unit-checks.ts && tsx test/installer-checks.ts && tsx test/detail-read-checks.ts && node test/installer-session-checks.mjs",
|
|
29
29
|
"check:plan-two:client": "tsx test/mcp-client-checks.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"types": "./dist/index.d.ts",
|
|
49
49
|
"files": [
|
|
50
50
|
"dist",
|
|
51
|
+
"installer",
|
|
51
52
|
"extensions",
|
|
52
53
|
"skills",
|
|
53
54
|
"README.md"
|