@naisys/hub 3.0.0-beta.20 → 3.0.0-beta.22

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.
@@ -1,4 +1,4 @@
1
- import { buildClientConfig } from "@naisys/common";
1
+ import { buildClientConfig, builtInImageModels, builtInLlmModels, } from "@naisys/common";
2
2
  import { HubEvents } from "@naisys/hub-protocol";
3
3
  import dotenv from "dotenv";
4
4
  /** Pushes the global config to NAISYS instances when they connect or when variables change */
@@ -7,6 +7,10 @@ export async function createHubConfigService(naisysServer, { hubDb }, logService
7
7
  success: false,
8
8
  error: "Not yet loaded",
9
9
  };
10
+ // API key variable names referenced by built-in models — always sensitive
11
+ const sensitiveKeys = new Set([...builtInLlmModels, ...builtInImageModels]
12
+ .map((m) => m.apiKeyVar)
13
+ .filter(Boolean));
10
14
  // Seed DB from .env on first run
11
15
  const existing = await hubDb.variables.findMany();
12
16
  if (existing.length > 0) {
@@ -22,6 +26,7 @@ export async function createHubConfigService(naisysServer, { hubDb }, logService
22
26
  data: entries.map(([key, value]) => ({
23
27
  key,
24
28
  value,
29
+ sensitive: sensitiveKeys.has(key),
25
30
  created_by: "hub",
26
31
  updated_by: "hub",
27
32
  })),
@@ -89,7 +94,9 @@ export async function createHubConfigService(naisysServer, { hubDb }, logService
89
94
  getConfig: () => cachedConfig,
90
95
  };
91
96
  }
92
- /** Create variable placeholders if they don't already exist */
97
+ /** Create variable placeholders if they don't already exist.
98
+ * If a variable already exists and `sensitive` is explicitly true,
99
+ * upgrade it to sensitive (e.g. API keys seeded before model init). */
93
100
  export async function ensureVariables(hubDb, keys) {
94
101
  for (const { key, sensitive } of keys) {
95
102
  const existing = await hubDb.variables.findUnique({ where: { key } });
@@ -105,6 +112,12 @@ export async function ensureVariables(hubDb, keys) {
105
112
  },
106
113
  });
107
114
  }
115
+ else if (sensitive && !existing.sensitive) {
116
+ await hubDb.variables.update({
117
+ where: { key },
118
+ data: { sensitive: true },
119
+ });
120
+ }
108
121
  }
109
122
  }
110
123
  //# sourceMappingURL=hubConfigService.js.map
@@ -11,7 +11,11 @@ export function createHubHostService(naisysServer, hostRegistrar, logService) {
11
11
  const hosts = hostRegistrar.getAllHosts().map((h) => {
12
12
  const client = clientByHostId.get(h.hostId);
13
13
  return {
14
- ...h,
14
+ hostId: h.hostId,
15
+ hostName: h.hostName,
16
+ machineId: h.machineId,
17
+ restricted: h.restricted,
18
+ hostType: h.hostType,
15
19
  online: !!client,
16
20
  version: client?.getClientVersion() || "",
17
21
  };
package/dist/naisysHub.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createDualLogger, ensureDotEnv, expandNaisysFolder, runSetupWizard, } from "@naisys/common-node";
1
+ import { createDualLogger, cwdWithTilde, ensureDotEnv, expandNaisysFolder, runSetupWizard, } from "@naisys/common-node";
2
2
  import { createHubDatabaseService } from "@naisys/hub-database";
3
3
  import { program } from "commander";
4
4
  import dotenv from "dotenv";
@@ -28,6 +28,7 @@ import { createNaisysServer } from "./services/naisysServer.js";
28
28
  */
29
29
  export const startHub = async (startupType, startSupervisor, plugins, startupAgentPath) => {
30
30
  try {
31
+ const agentPath = startupAgentPath || ".";
31
32
  // Create log service first
32
33
  const logService = createDualLogger("hub-server.log");
33
34
  logService.log(`[Hub] Starting Hub server in ${startupType} mode...`);
@@ -39,7 +40,7 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
39
40
  // Schema version for sync protocol - should match NAISYS instance
40
41
  const hubDatabaseService = await createHubDatabaseService();
41
42
  // Seed database with agent configs from yaml files (one-time, skips if non-empty)
42
- await seedAgentConfigs(hubDatabaseService, logService, startupAgentPath);
43
+ await seedAgentConfigs(hubDatabaseService, logService, agentPath);
43
44
  // Create host registrar for tracking NAISYS instance connections
44
45
  const hostRegistrar = await createHostRegistrar(hubDatabaseService);
45
46
  // Create Fastify instance (TLS is handled by the reverse proxy)
@@ -117,7 +118,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
117
118
  type: "fields",
118
119
  comment: "Hub server configuration",
119
120
  fields: [
120
- { key: "NAISYS_FOLDER", label: "NAISYS Data Folder" },
121
+ { key: "NAISYS_FOLDER", label: "NAISYS Data Folder", defaultValue: cwdWithTilde() },
121
122
  { key: "SERVER_PORT", label: "Server Port" },
122
123
  ],
123
124
  },
@@ -127,7 +128,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
127
128
  if (process.argv.includes("--setup")) {
128
129
  const { default: path } = await import("path");
129
130
  await runSetupWizard(path.resolve(".env"), hubExampleUrl, hubWizardConfig);
130
- process.exit(0);
131
+ expandNaisysFolder();
131
132
  }
132
133
  await ensureDotEnv(hubExampleUrl, hubWizardConfig);
133
134
  expandNaisysFolder();
@@ -140,6 +141,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
140
141
  const plugins = [];
141
142
  if (program.opts().erp)
142
143
  plugins.push("erp");
143
- void startHub("standalone", program.opts().supervisor, plugins, program.args[0] || ".");
144
+ void startHub("standalone", program.opts().supervisor, plugins, program.args[0]);
144
145
  }
145
146
  //# sourceMappingURL=naisysHub.js.map
@@ -1,4 +1,5 @@
1
1
  import { toUrlSafeKey } from "@naisys/common";
2
+ import crypto from "node:crypto";
2
3
  export async function createHostRegistrar({ hubDb }) {
3
4
  /** Cache of all known hosts keyed by id */
4
5
  const hostsById = new Map();
@@ -7,26 +8,47 @@ export async function createHostRegistrar({ hubDb }) {
7
8
  select: {
8
9
  id: true,
9
10
  name: true,
11
+ machine_id: true,
10
12
  restricted: true,
11
13
  host_type: true,
12
14
  last_version: true,
15
+ environment: true,
13
16
  },
14
17
  });
15
18
  for (const row of rows) {
16
19
  hostsById.set(row.id, {
17
20
  hostName: row.name,
21
+ machineId: row.machine_id ?? "",
18
22
  restricted: row.restricted,
19
23
  hostType: row.host_type,
20
24
  lastVersion: row.last_version ?? "",
25
+ environment: row.environment,
21
26
  });
22
27
  }
28
+ /** Helper to update last_active and connection metadata for an existing host */
29
+ function updateCache(hostId, entry) {
30
+ hostsById.set(hostId, entry);
31
+ }
23
32
  /**
24
- * Register a NAISYS instance by name. Creates a new record if not found,
25
- * updates last_active on every call.
26
- * @returns The host's autoincrement id
33
+ * Find the next available hostname when a collision occurs.
34
+ * E.g. "myhost" "myhost-2", "myhost-3", etc.
27
35
  */
28
- async function registerHost(hostName, hostType, lastIp, clientVersion) {
36
+ async function findAvailableHostName(baseName) {
37
+ let suffix = 2;
38
+ while (true) {
39
+ const candidate = `${baseName}-${suffix}`;
40
+ const exists = await hubDb.hosts.findUnique({
41
+ where: { name: candidate },
42
+ });
43
+ if (!exists)
44
+ return candidate;
45
+ suffix++;
46
+ }
47
+ }
48
+ /** Register a supervisor connection. Simple name-based upsert, no machineId. */
49
+ async function registerSupervisor(hostName, lastIp, clientVersion) {
29
50
  hostName = toUrlSafeKey(hostName);
51
+ const hostType = "supervisor";
30
52
  const existing = await hubDb.hosts.findUnique({
31
53
  where: { name: hostName },
32
54
  });
@@ -40,39 +62,147 @@ export async function createHostRegistrar({ hubDb }) {
40
62
  last_version: clientVersion,
41
63
  },
42
64
  });
43
- hostsById.set(existing.id, {
65
+ updateCache(existing.id, {
44
66
  hostName,
67
+ machineId: existing.machine_id ?? "",
45
68
  restricted: existing.restricted,
46
69
  hostType,
47
70
  lastVersion: clientVersion,
71
+ environment: existing.environment,
48
72
  });
49
- return existing.id;
73
+ return { hostId: existing.id, machineId: "", hostName };
74
+ }
75
+ const created = await hubDb.hosts.create({
76
+ data: {
77
+ name: hostName,
78
+ host_type: hostType,
79
+ last_ip: lastIp,
80
+ last_version: clientVersion,
81
+ last_active: new Date().toISOString(),
82
+ },
83
+ });
84
+ updateCache(created.id, {
85
+ hostName,
86
+ machineId: "",
87
+ restricted: false,
88
+ hostType,
89
+ lastVersion: clientVersion,
90
+ environment: null,
91
+ });
92
+ return { hostId: created.id, machineId: "", hostName };
93
+ }
94
+ /**
95
+ * Register a NAISYS client. If machineId is provided, looks up by machineId
96
+ * first (the DB hostname is authoritative after a rename). Otherwise looks up
97
+ * by hostname, deduplicating with a -N suffix on collision.
98
+ *
99
+ * @returns The host's id, assigned machineId, and authoritative hostname
100
+ */
101
+ async function registerNaisysClient(hostName, machineId, lastIp, clientVersion, environment) {
102
+ hostName = toUrlSafeKey(hostName);
103
+ const hostType = "naisys";
104
+ const environmentJson = environment ? JSON.stringify(environment) : null;
105
+ // --- Lookup by machineId (returning client) ---
106
+ if (machineId) {
107
+ const byMachineId = await hubDb.hosts.findUnique({
108
+ where: { machine_id: machineId },
109
+ });
110
+ if (byMachineId) {
111
+ await hubDb.hosts.update({
112
+ where: { id: byMachineId.id },
113
+ data: {
114
+ last_active: new Date().toISOString(),
115
+ host_type: hostType,
116
+ last_ip: lastIp,
117
+ last_version: clientVersion,
118
+ ...(environmentJson !== null
119
+ ? { environment: environmentJson }
120
+ : {}),
121
+ },
122
+ });
123
+ updateCache(byMachineId.id, {
124
+ hostName: byMachineId.name,
125
+ machineId,
126
+ restricted: byMachineId.restricted,
127
+ hostType,
128
+ lastVersion: clientVersion,
129
+ environment: environmentJson ?? byMachineId.environment,
130
+ });
131
+ // Return the DB hostname (may differ from what the client sent if renamed)
132
+ return {
133
+ hostId: byMachineId.id,
134
+ machineId,
135
+ hostName: byMachineId.name,
136
+ };
137
+ }
138
+ // machineId not found in DB — fall through to name-based lookup
139
+ }
140
+ // --- Lookup by hostname ---
141
+ const newMachineId = machineId || crypto.randomUUID();
142
+ const byName = await hubDb.hosts.findUnique({
143
+ where: { name: hostName },
144
+ });
145
+ if (byName) {
146
+ if (!byName.machine_id) {
147
+ // Existing host without a machineId (pre-migration) — adopt it
148
+ await hubDb.hosts.update({
149
+ where: { id: byName.id },
150
+ data: {
151
+ machine_id: newMachineId,
152
+ last_active: new Date().toISOString(),
153
+ host_type: hostType,
154
+ last_ip: lastIp,
155
+ last_version: clientVersion,
156
+ ...(environmentJson !== null
157
+ ? { environment: environmentJson }
158
+ : {}),
159
+ },
160
+ });
161
+ updateCache(byName.id, {
162
+ hostName,
163
+ machineId: newMachineId,
164
+ restricted: byName.restricted,
165
+ hostType,
166
+ lastVersion: clientVersion,
167
+ environment: environmentJson ?? byName.environment,
168
+ });
169
+ return { hostId: byName.id, machineId: newMachineId, hostName };
170
+ }
171
+ // Name collision with a different machine — deduplicate
172
+ hostName = await findAvailableHostName(hostName);
50
173
  }
174
+ // --- Create new host ---
51
175
  const created = await hubDb.hosts.create({
52
176
  data: {
53
177
  name: hostName,
178
+ machine_id: newMachineId,
54
179
  host_type: hostType,
55
180
  last_ip: lastIp,
56
181
  last_version: clientVersion,
182
+ environment: environmentJson,
57
183
  last_active: new Date().toISOString(),
58
184
  },
59
185
  });
60
- hostsById.set(created.id, {
186
+ updateCache(created.id, {
61
187
  hostName,
188
+ machineId: newMachineId,
62
189
  restricted: false,
63
190
  hostType,
64
191
  lastVersion: clientVersion,
192
+ environment: environmentJson,
65
193
  });
66
- return created.id;
194
+ return { hostId: created.id, machineId: newMachineId, hostName };
67
195
  }
68
196
  /** Returns all known hosts (from DB + any newly registered) */
69
197
  function getAllHosts() {
70
198
  return Array.from(hostsById, ([hostId, entry]) => ({
71
199
  hostId,
72
200
  hostName: entry.hostName,
201
+ machineId: entry.machineId,
73
202
  restricted: entry.restricted,
74
203
  hostType: entry.hostType,
75
204
  lastVersion: entry.lastVersion,
205
+ environment: entry.environment,
76
206
  }));
77
207
  }
78
208
  /** Re-read all hosts from DB and replace the in-memory cache */
@@ -81,23 +211,28 @@ export async function createHostRegistrar({ hubDb }) {
81
211
  select: {
82
212
  id: true,
83
213
  name: true,
214
+ machine_id: true,
84
215
  restricted: true,
85
216
  host_type: true,
86
217
  last_version: true,
218
+ environment: true,
87
219
  },
88
220
  });
89
221
  hostsById.clear();
90
222
  for (const row of rows) {
91
223
  hostsById.set(row.id, {
92
224
  hostName: row.name,
225
+ machineId: row.machine_id ?? "",
93
226
  restricted: row.restricted,
94
227
  hostType: row.host_type,
95
228
  lastVersion: row.last_version ?? "",
229
+ environment: row.environment,
96
230
  });
97
231
  }
98
232
  }
99
233
  return {
100
- registerHost,
234
+ registerNaisysClient,
235
+ registerSupervisor,
101
236
  getAllHosts,
102
237
  refreshHosts,
103
238
  };
@@ -1,3 +1,4 @@
1
+ import { HubEvents } from "@naisys/hub-protocol";
1
2
  import { createNaisysConnection } from "./naisysConnection.js";
2
3
  /**
3
4
  * Creates the NAISYS namespace server that accepts NAISYS instance connections.
@@ -59,7 +60,7 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
59
60
  }
60
61
  // Authentication middleware
61
62
  nsp.use(async (socket, next) => {
62
- const { hubAccessKey: clientAccessKey, hostName, hostType: rawHostType, clientVersion, } = socket.handshake.auth;
63
+ const { hubAccessKey: clientAccessKey, hostName, machineId: rawMachineId, hostType: rawHostType, clientVersion, environment: rawEnvironment, } = socket.handshake.auth;
63
64
  if (!clientAccessKey || clientAccessKey !== hubAccessKey) {
64
65
  logService.log(`[Hub] Connection rejected: invalid access key from ${socket.handshake.address}`);
65
66
  return next(new Error("Invalid access key"));
@@ -71,14 +72,23 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
71
72
  try {
72
73
  const hostType = (typeof rawHostType === "string" ? rawHostType : "naisys");
73
74
  const resolvedVersion = typeof clientVersion === "string" ? clientVersion : "";
74
- const hostId = await hostRegistrar.registerHost(hostName, hostType, socket.handshake.address, resolvedVersion);
75
+ const machineId = typeof rawMachineId === "string" && rawMachineId
76
+ ? rawMachineId
77
+ : undefined;
78
+ const environment = rawEnvironment && typeof rawEnvironment === "object"
79
+ ? rawEnvironment
80
+ : undefined;
81
+ const result = hostType === "supervisor"
82
+ ? await hostRegistrar.registerSupervisor(hostName, socket.handshake.address, resolvedVersion)
83
+ : await hostRegistrar.registerNaisysClient(hostName, machineId, socket.handshake.address, resolvedVersion, environment);
75
84
  // Reject duplicate naisys connections (supervisors may have multiple)
76
- if (hostType === "naisys" && naisysConnections.has(hostId)) {
77
- logService.log(`[Hub] Connection rejected: host '${hostName}' is already connected`);
78
- return next(new Error(`Host '${hostName}' already has an active connection`));
85
+ if (hostType === "naisys" && naisysConnections.has(result.hostId)) {
86
+ logService.log(`[Hub] Connection rejected: host '${result.hostName}' is already connected`);
87
+ return next(new Error(`Host '${result.hostName}' already has an active connection`));
79
88
  }
80
- socket.data.hostId = hostId;
81
- socket.data.hostName = hostName;
89
+ socket.data.hostId = result.hostId;
90
+ socket.data.hostName = result.hostName;
91
+ socket.data.machineId = result.machineId;
82
92
  socket.data.hostType = hostType;
83
93
  socket.data.clientVersion = resolvedVersion;
84
94
  next();
@@ -90,7 +100,10 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
90
100
  });
91
101
  // Handle new connections
92
102
  nsp.on("connection", (socket) => {
93
- const { hostId, hostName, hostType, clientVersion } = socket.data;
103
+ const { hostId, hostName, machineId, hostType, clientVersion } = socket.data;
104
+ // Send the client its assigned machineId and authoritative hostname
105
+ const registered = { machineId, hostName };
106
+ socket.emit(HubEvents.HOST_REGISTERED, registered);
94
107
  // Create connection handler for this socket, passing our emit function
95
108
  const connection = createNaisysConnection(socket, {
96
109
  hostId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/hub",
3
- "version": "3.0.0-beta.20",
3
+ "version": "3.0.0-beta.22",
4
4
  "description": "NAISYS Hub - Adds persistence and multi-instance coordination to NAISYS",
5
5
  "type": "module",
6
6
  "main": "dist/naisysHub.js",
@@ -30,7 +30,7 @@
30
30
  "!dist/**/*.d.ts.map"
31
31
  ],
32
32
  "peerDependencies": {
33
- "@naisys/supervisor": "3.0.0-beta.20"
33
+ "@naisys/supervisor": "3.0.0-beta.22"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@naisys/supervisor": {
@@ -38,10 +38,10 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@naisys/common": "3.0.0-beta.20",
42
- "@naisys/common-node": "3.0.0-beta.20",
43
- "@naisys/hub-database": "3.0.0-beta.20",
44
- "@naisys/hub-protocol": "3.0.0-beta.20",
41
+ "@naisys/common": "3.0.0-beta.22",
42
+ "@naisys/common-node": "3.0.0-beta.22",
43
+ "@naisys/hub-database": "3.0.0-beta.22",
44
+ "@naisys/hub-protocol": "3.0.0-beta.22",
45
45
  "commander": "^14.0.3",
46
46
  "dotenv": "^17.3.1",
47
47
  "fastify": "^5.8.2",