@adhdev/daemon-core 0.9.82-rc.48 → 0.9.82-rc.49
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/dist/commands/router.d.ts +1 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +954 -212
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +928 -198
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +8 -4
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/repo-mesh-types.d.ts +32 -0
- package/package.json +1 -1
- package/src/commands/router.ts +412 -178
- package/src/config/mesh-config.ts +244 -1
- package/src/index.ts +23 -1
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +1 -0
- package/src/mesh/mesh-work-queue.ts +14 -3
- package/src/mesh/refine-config.ts +306 -0
- package/src/repo-mesh-types.ts +36 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
|
-
import { randomUUID } from 'crypto';
|
|
11
|
+
import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
12
12
|
import { getConfigDir } from './config.js';
|
|
13
13
|
import type {
|
|
14
14
|
LocalMeshConfig,
|
|
@@ -18,8 +18,11 @@ import type {
|
|
|
18
18
|
RepoMeshNodePolicy,
|
|
19
19
|
RepoMeshNodeCapabilities,
|
|
20
20
|
RepoMeshCoordinatorConfig,
|
|
21
|
+
RepoMeshHostMetadata,
|
|
22
|
+
RepoMeshDaemonRole,
|
|
21
23
|
} from '../repo-mesh-types.js';
|
|
22
24
|
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
25
|
+
import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
|
|
23
26
|
|
|
24
27
|
// ─── Persistence ────────────────────────────────
|
|
25
28
|
|
|
@@ -112,6 +115,7 @@ export interface CreateMeshOptions {
|
|
|
112
115
|
defaultBranch?: string;
|
|
113
116
|
policy?: Partial<RepoMeshPolicy>;
|
|
114
117
|
coordinator?: RepoMeshCoordinatorConfig;
|
|
118
|
+
meshHost?: RepoMeshHostMetadata;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
|
|
@@ -133,6 +137,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
|
|
|
133
137
|
defaultBranch: opts.defaultBranch,
|
|
134
138
|
policy: mergeMeshPolicy(undefined, opts.policy),
|
|
135
139
|
coordinator: opts.coordinator || {},
|
|
140
|
+
meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
|
|
136
141
|
nodes: [],
|
|
137
142
|
createdAt: now,
|
|
138
143
|
updatedAt: now,
|
|
@@ -148,6 +153,7 @@ export interface UpdateMeshOptions {
|
|
|
148
153
|
defaultBranch?: string;
|
|
149
154
|
policy?: Partial<RepoMeshPolicy>;
|
|
150
155
|
coordinator?: RepoMeshCoordinatorConfig;
|
|
156
|
+
meshHost?: RepoMeshHostMetadata;
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
|
|
@@ -159,6 +165,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
|
|
|
159
165
|
if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
|
|
160
166
|
if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
|
|
161
167
|
if (opts.coordinator) mesh.coordinator = opts.coordinator;
|
|
168
|
+
if (opts.meshHost) mesh.meshHost = opts.meshHost;
|
|
162
169
|
mesh.updatedAt = new Date().toISOString();
|
|
163
170
|
|
|
164
171
|
saveMeshConfig(config);
|
|
@@ -174,6 +181,240 @@ export function deleteMesh(meshId: string): boolean {
|
|
|
174
181
|
return true;
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function normalizeManualHostAddress(hostAddress: string): string {
|
|
185
|
+
const normalized = hostAddress.trim().replace(/\/+$/, '');
|
|
186
|
+
if (!normalized) throw new Error('hostAddress required');
|
|
187
|
+
let parsed: URL;
|
|
188
|
+
try {
|
|
189
|
+
parsed = new URL(normalized);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
|
|
192
|
+
}
|
|
193
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
|
|
194
|
+
throw new Error('hostAddress must use http, https, ws, or wss');
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function tokenIdForManualPairing(token: string): string {
|
|
200
|
+
return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeTokenExpiry(value: unknown): string | undefined {
|
|
204
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
205
|
+
const date = new Date(value);
|
|
206
|
+
if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
|
|
207
|
+
return date.toISOString();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
|
|
211
|
+
const token = rawToken.trim();
|
|
212
|
+
if (!token) return { ok: false, reason: 'token required' };
|
|
213
|
+
const presentedTokenId = tokenIdForManualPairing(token);
|
|
214
|
+
const expectedTokenId = pairing?.tokenId;
|
|
215
|
+
if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
|
|
216
|
+
return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
|
|
217
|
+
}
|
|
218
|
+
if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
|
|
219
|
+
return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
|
|
220
|
+
}
|
|
221
|
+
if (presentedTokenId !== expectedTokenId) {
|
|
222
|
+
return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, tokenId: presentedTokenId };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ConfigureMeshHostPairingOptions {
|
|
228
|
+
hostAddress: string;
|
|
229
|
+
token: string;
|
|
230
|
+
now?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function configureMeshHostPairing(
|
|
234
|
+
meshId: string,
|
|
235
|
+
opts: ConfigureMeshHostPairingOptions,
|
|
236
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
|
|
237
|
+
const hostAddress = normalizeManualHostAddress(opts.hostAddress);
|
|
238
|
+
const token = opts.token.trim();
|
|
239
|
+
if (!token) throw new Error('token required');
|
|
240
|
+
|
|
241
|
+
const config = loadMeshConfig();
|
|
242
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
243
|
+
if (!mesh) return undefined;
|
|
244
|
+
|
|
245
|
+
const now = opts.now || new Date().toISOString();
|
|
246
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
247
|
+
const meshHost: RepoMeshHostMetadata = {
|
|
248
|
+
...previous,
|
|
249
|
+
role: 'member',
|
|
250
|
+
hostAddress,
|
|
251
|
+
pairing: {
|
|
252
|
+
status: 'pairing',
|
|
253
|
+
tokenId: tokenIdForManualPairing(token),
|
|
254
|
+
lastPairedAt: now,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
mesh.meshHost = meshHost;
|
|
259
|
+
mesh.updatedAt = now;
|
|
260
|
+
saveMeshConfig(config);
|
|
261
|
+
return { mesh, meshHost, hostAddress };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface CreateMeshHostPairingTokenOptions {
|
|
265
|
+
token?: string;
|
|
266
|
+
expiresAt?: string;
|
|
267
|
+
now?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createMeshHostPairingToken(
|
|
271
|
+
meshId: string,
|
|
272
|
+
opts: CreateMeshHostPairingTokenOptions = {},
|
|
273
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
|
|
274
|
+
const config = loadMeshConfig();
|
|
275
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
276
|
+
if (!mesh) return undefined;
|
|
277
|
+
const now = opts.now || new Date().toISOString();
|
|
278
|
+
const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
|
|
279
|
+
if (!token) throw new Error('token required');
|
|
280
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
281
|
+
const expiresAt = normalizeTokenExpiry(opts.expiresAt);
|
|
282
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
283
|
+
if (previous.role === 'member') {
|
|
284
|
+
throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
|
|
285
|
+
}
|
|
286
|
+
const meshHost: RepoMeshHostMetadata = {
|
|
287
|
+
...previous,
|
|
288
|
+
role: 'host',
|
|
289
|
+
pairing: {
|
|
290
|
+
status: 'pairing',
|
|
291
|
+
tokenId,
|
|
292
|
+
lastPairedAt: now,
|
|
293
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
mesh.meshHost = meshHost;
|
|
297
|
+
mesh.updatedAt = now;
|
|
298
|
+
saveMeshConfig(config);
|
|
299
|
+
return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface MeshHostJoinMemberNodeInput {
|
|
303
|
+
id?: string;
|
|
304
|
+
workspace: string;
|
|
305
|
+
repoRoot?: string;
|
|
306
|
+
daemonId?: string;
|
|
307
|
+
machineId?: string;
|
|
308
|
+
userOverrides?: Partial<RepoMeshNodeCapabilities>;
|
|
309
|
+
policy?: RepoMeshNodePolicy;
|
|
310
|
+
role?: RepoMeshDaemonRole;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface ApplyMeshHostJoinOptions {
|
|
314
|
+
token: string;
|
|
315
|
+
memberNode: MeshHostJoinMemberNodeInput;
|
|
316
|
+
memberMeshId?: string;
|
|
317
|
+
now?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function applyMeshHostJoinRequest(
|
|
321
|
+
meshId: string,
|
|
322
|
+
opts: ApplyMeshHostJoinOptions,
|
|
323
|
+
): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
|
|
324
|
+
const config = loadMeshConfig();
|
|
325
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
326
|
+
if (!mesh) return undefined;
|
|
327
|
+
const now = opts.now || new Date().toISOString();
|
|
328
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
329
|
+
if (previous.role === 'member') {
|
|
330
|
+
return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
|
|
331
|
+
}
|
|
332
|
+
const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
|
|
333
|
+
const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
|
|
334
|
+
if (!validation.ok) {
|
|
335
|
+
mesh.meshHost = {
|
|
336
|
+
...meshHost,
|
|
337
|
+
pairing: {
|
|
338
|
+
...(meshHost.pairing || { status: 'not_configured' as const }),
|
|
339
|
+
status: 'rejected',
|
|
340
|
+
lastRejectedAt: now,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
mesh.updatedAt = now;
|
|
344
|
+
saveMeshConfig(config);
|
|
345
|
+
return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const workspace = opts.memberNode.workspace.trim();
|
|
349
|
+
if (!workspace) throw new Error('memberNode.workspace required');
|
|
350
|
+
const memberId = opts.memberNode.id?.trim();
|
|
351
|
+
let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
|
|
352
|
+
if (node) {
|
|
353
|
+
node.workspace = workspace;
|
|
354
|
+
node.repoRoot = opts.memberNode.repoRoot;
|
|
355
|
+
node.daemonId = opts.memberNode.daemonId;
|
|
356
|
+
node.machineId = opts.memberNode.machineId;
|
|
357
|
+
node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
|
|
358
|
+
node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
|
|
359
|
+
node.role = 'member';
|
|
360
|
+
} else {
|
|
361
|
+
if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
|
|
362
|
+
node = {
|
|
363
|
+
id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
|
|
364
|
+
workspace,
|
|
365
|
+
repoRoot: opts.memberNode.repoRoot,
|
|
366
|
+
daemonId: opts.memberNode.daemonId,
|
|
367
|
+
machineId: opts.memberNode.machineId,
|
|
368
|
+
userOverrides: opts.memberNode.userOverrides || {},
|
|
369
|
+
policy: opts.memberNode.policy || {},
|
|
370
|
+
role: 'member',
|
|
371
|
+
};
|
|
372
|
+
mesh.nodes.push(node);
|
|
373
|
+
}
|
|
374
|
+
mesh.meshHost = {
|
|
375
|
+
...meshHost,
|
|
376
|
+
pairing: {
|
|
377
|
+
...(meshHost.pairing || {}),
|
|
378
|
+
status: 'paired',
|
|
379
|
+
tokenId: validation.tokenId,
|
|
380
|
+
joinedAt: now,
|
|
381
|
+
lastPairedAt: meshHost.pairing?.lastPairedAt || now,
|
|
382
|
+
...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
mesh.updatedAt = now;
|
|
386
|
+
saveMeshConfig(config);
|
|
387
|
+
return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function markMeshHostPairingJoined(
|
|
391
|
+
meshId: string,
|
|
392
|
+
opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
|
|
393
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
|
|
394
|
+
const config = loadMeshConfig();
|
|
395
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
396
|
+
if (!mesh) return undefined;
|
|
397
|
+
const now = opts.joinedAt || new Date().toISOString();
|
|
398
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
399
|
+
const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
|
|
400
|
+
mesh.meshHost = {
|
|
401
|
+
...previous,
|
|
402
|
+
role: 'member',
|
|
403
|
+
...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
|
|
404
|
+
...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
|
|
405
|
+
pairing: {
|
|
406
|
+
...(previous.pairing || {}),
|
|
407
|
+
status: 'paired',
|
|
408
|
+
...(tokenId ? { tokenId } : {}),
|
|
409
|
+
joinedAt: now,
|
|
410
|
+
lastPairedAt: previous.pairing?.lastPairedAt || now,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
mesh.updatedAt = now;
|
|
414
|
+
saveMeshConfig(config);
|
|
415
|
+
return { mesh, meshHost: mesh.meshHost };
|
|
416
|
+
}
|
|
417
|
+
|
|
177
418
|
// ─── Node Operations ────────────────────────────
|
|
178
419
|
|
|
179
420
|
export interface AddNodeOptions {
|
|
@@ -186,6 +427,7 @@ export interface AddNodeOptions {
|
|
|
186
427
|
isLocalWorktree?: boolean;
|
|
187
428
|
worktreeBranch?: string;
|
|
188
429
|
clonedFromNodeId?: string;
|
|
430
|
+
role?: RepoMeshDaemonRole;
|
|
189
431
|
}
|
|
190
432
|
|
|
191
433
|
export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
|
|
@@ -213,6 +455,7 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
|
|
|
213
455
|
isLocalWorktree: opts.isLocalWorktree,
|
|
214
456
|
worktreeBranch: opts.worktreeBranch,
|
|
215
457
|
clonedFromNodeId: opts.clonedFromNodeId,
|
|
458
|
+
role: opts.role,
|
|
216
459
|
};
|
|
217
460
|
|
|
218
461
|
mesh.nodes.push(node);
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,10 @@ export type {
|
|
|
88
88
|
// ── Repo Mesh Types (cross-package) ──
|
|
89
89
|
export type {
|
|
90
90
|
RepoMesh,
|
|
91
|
+
RepoMeshDaemonRole,
|
|
92
|
+
RepoMeshHostMetadata,
|
|
93
|
+
RepoMeshHostPairingMetadata,
|
|
94
|
+
RepoMeshHostStatus,
|
|
91
95
|
RepoMeshNode,
|
|
92
96
|
RepoMeshNodeHealth,
|
|
93
97
|
RepoMeshPolicy,
|
|
@@ -155,6 +159,21 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
|
|
|
155
159
|
// ── Mesh Coordinator ──
|
|
156
160
|
export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
|
|
157
161
|
export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
|
|
162
|
+
export {
|
|
163
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
164
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
165
|
+
loadMeshRefineConfig,
|
|
166
|
+
resolveMeshRefineValidationPlan,
|
|
167
|
+
suggestMeshRefineConfig,
|
|
168
|
+
validateMeshRefineConfig,
|
|
169
|
+
} from './mesh/refine-config.js';
|
|
170
|
+
export type {
|
|
171
|
+
MeshRefineValidationCategory,
|
|
172
|
+
MeshRefineValidationCommandPlan,
|
|
173
|
+
MeshRefineValidationPlan,
|
|
174
|
+
RepoMeshRefineConfig,
|
|
175
|
+
RepoMeshRefineValidationCommandConfig,
|
|
176
|
+
} from './mesh/refine-config.js';
|
|
158
177
|
export { syncMeshes } from './mesh/mesh-sync.js';
|
|
159
178
|
export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
|
|
160
179
|
|
|
@@ -166,7 +185,10 @@ export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshL
|
|
|
166
185
|
|
|
167
186
|
// ── Mesh Work Queue (GUPP) ──
|
|
168
187
|
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
|
|
169
|
-
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
|
|
188
|
+
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats, MeshQueueMutationOptions } from './mesh/mesh-work-queue.js';
|
|
189
|
+
|
|
190
|
+
// ── Mesh Host Ownership ──
|
|
191
|
+
export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHostOwner, normalizeMeshDaemonRole, requireMeshHostQueueOwner, resolveMeshHostStatus } from './mesh/mesh-host-ownership.js';
|
|
170
192
|
|
|
171
193
|
// ── Mesh Visualization ──
|
|
172
194
|
// buildMeshGraph and MeshGraph types moved to @adhdev/web-core to avoid
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { RepoMeshDaemonRole, RepoMeshHostMetadata, RepoMeshHostStatus } from '../repo-mesh-types.js';
|
|
2
|
+
|
|
3
|
+
function readObject(value: unknown): Record<string, unknown> | null {
|
|
4
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readString(value: unknown): string | undefined {
|
|
8
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeMeshDaemonRole(value: unknown): RepoMeshDaemonRole | undefined {
|
|
12
|
+
return value === 'host' || value === 'member' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveMeshHostStatus(mesh: unknown): RepoMeshHostStatus {
|
|
16
|
+
const meshRecord = readObject(mesh);
|
|
17
|
+
const raw = readObject(meshRecord?.meshHost);
|
|
18
|
+
const role = normalizeMeshDaemonRole(raw?.role) ?? 'host';
|
|
19
|
+
const pairing = readObject(raw?.pairing);
|
|
20
|
+
const normalized: RepoMeshHostStatus = {
|
|
21
|
+
role,
|
|
22
|
+
canOwnCoordinator: role === 'host',
|
|
23
|
+
canOwnQueue: role === 'host',
|
|
24
|
+
defaulted: !raw,
|
|
25
|
+
};
|
|
26
|
+
const hostDaemonId = readString(raw?.hostDaemonId);
|
|
27
|
+
const hostNodeId = readString(raw?.hostNodeId);
|
|
28
|
+
const hostAddress = readString(raw?.hostAddress);
|
|
29
|
+
if (hostDaemonId) normalized.hostDaemonId = hostDaemonId;
|
|
30
|
+
if (hostNodeId) normalized.hostNodeId = hostNodeId;
|
|
31
|
+
if (hostAddress) normalized.hostAddress = hostAddress;
|
|
32
|
+
if (pairing) {
|
|
33
|
+
const status = pairing.status === 'pairing' || pairing.status === 'paired' || pairing.status === 'rejected' || pairing.status === 'revoked'
|
|
34
|
+
? pairing.status
|
|
35
|
+
: 'not_configured';
|
|
36
|
+
normalized.pairing = {
|
|
37
|
+
status,
|
|
38
|
+
...(readString(pairing.tokenId) ? { tokenId: readString(pairing.tokenId) } : {}),
|
|
39
|
+
...(readString(pairing.joinedAt) ? { joinedAt: readString(pairing.joinedAt) } : {}),
|
|
40
|
+
...(readString(pairing.lastPairedAt) ? { lastPairedAt: readString(pairing.lastPairedAt) } : {}),
|
|
41
|
+
...(readString(pairing.lastRejectedAt) ? { lastRejectedAt: readString(pairing.lastRejectedAt) } : {}),
|
|
42
|
+
...(readString(pairing.expiresAt) ? { expiresAt: readString(pairing.expiresAt) } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isMeshHostOwner(mesh: unknown): boolean {
|
|
49
|
+
return resolveMeshHostStatus(mesh).role === 'host';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildMeshHostRequiredFailure(mesh: unknown, operation: string): Record<string, unknown> {
|
|
53
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
code: 'mesh_host_required',
|
|
57
|
+
error: `Mesh Host daemon required for ${operation}; member daemons must pair with the host and cannot own coordinator/queue mutations.`,
|
|
58
|
+
meshHost,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function requireMeshHostQueueOwner(opts?: { ownerRole?: RepoMeshDaemonRole }): void {
|
|
63
|
+
if (opts?.ownerRole === 'member') {
|
|
64
|
+
throw new Error('Mesh Host daemon required to mutate mesh queue; member daemons must use the host-owned queue.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createDefaultMeshHostMetadata(): RepoMeshHostMetadata {
|
|
69
|
+
return {
|
|
70
|
+
role: 'host',
|
|
71
|
+
pairing: { status: 'not_configured' },
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSyn
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
5
|
+
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
6
|
+
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
5
7
|
|
|
6
8
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
9
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
@@ -45,6 +47,10 @@ export interface MeshWorkQueueEntry {
|
|
|
45
47
|
updatedAt: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export interface MeshQueueMutationOptions {
|
|
51
|
+
ownerRole?: RepoMeshDaemonRole;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
function getQueuePath(meshId: string): string {
|
|
49
55
|
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
50
56
|
return join(getLedgerDir(), `${safe}.queue.json`);
|
|
@@ -97,8 +103,9 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
97
103
|
export function enqueueTask(
|
|
98
104
|
meshId: string,
|
|
99
105
|
message: string,
|
|
100
|
-
opts?: { targetNodeId?: string; targetSessionId?: string }
|
|
106
|
+
opts?: { targetNodeId?: string; targetSessionId?: string } & MeshQueueMutationOptions,
|
|
101
107
|
): MeshWorkQueueEntry {
|
|
108
|
+
requireMeshHostQueueOwner(opts);
|
|
102
109
|
return withQueueLock(meshId, () => {
|
|
103
110
|
const queue = readQueue(meshId);
|
|
104
111
|
const entry: MeshWorkQueueEntry = {
|
|
@@ -166,7 +173,9 @@ export function updateTaskStatus(
|
|
|
166
173
|
meshId: string,
|
|
167
174
|
taskId: string,
|
|
168
175
|
status: MeshTaskStatus,
|
|
176
|
+
opts?: MeshQueueMutationOptions,
|
|
169
177
|
): MeshWorkQueueEntry | null {
|
|
178
|
+
requireMeshHostQueueOwner(opts);
|
|
170
179
|
return withQueueLock(meshId, () => {
|
|
171
180
|
const queue = readQueue(meshId);
|
|
172
181
|
const idx = queue.findIndex(q => q.id === taskId);
|
|
@@ -201,8 +210,9 @@ export function recordTaskAutoLaunch(
|
|
|
201
210
|
export function cancelTask(
|
|
202
211
|
meshId: string,
|
|
203
212
|
taskId: string,
|
|
204
|
-
opts?: { reason?: string },
|
|
213
|
+
opts?: { reason?: string } & MeshQueueMutationOptions,
|
|
205
214
|
): MeshWorkQueueEntry | null {
|
|
215
|
+
requireMeshHostQueueOwner(opts);
|
|
206
216
|
return withQueueLock(meshId, () => {
|
|
207
217
|
const queue = readQueue(meshId);
|
|
208
218
|
const idx = queue.findIndex(q => q.id === taskId);
|
|
@@ -230,8 +240,9 @@ export function requeueTask(
|
|
|
230
240
|
targetSessionId?: string;
|
|
231
241
|
clearTargetNode?: boolean;
|
|
232
242
|
clearTargetSession?: boolean;
|
|
233
|
-
},
|
|
243
|
+
} & MeshQueueMutationOptions,
|
|
234
244
|
): MeshWorkQueueEntry | null {
|
|
245
|
+
requireMeshHostQueueOwner(opts);
|
|
235
246
|
return withQueueLock(meshId, () => {
|
|
236
247
|
const queue = readQueue(meshId);
|
|
237
248
|
const idx = queue.findIndex(q => q.id === taskId);
|