@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
package/src/commands/router.ts
CHANGED
|
@@ -39,6 +39,16 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
|
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
41
|
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
42
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
|
+
import {
|
|
44
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
45
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
46
|
+
loadMeshRefineConfig,
|
|
47
|
+
resolveMeshRefineValidationPlan,
|
|
48
|
+
suggestMeshRefineConfig,
|
|
49
|
+
validateMeshRefineConfig,
|
|
50
|
+
type MeshRefineValidationCommandPlan,
|
|
51
|
+
} from '../mesh/refine-config.js';
|
|
42
52
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
53
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
54
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
@@ -859,13 +869,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
859
869
|
}
|
|
860
870
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
861
871
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
862
|
-
type MeshRefineValidationCommand =
|
|
863
|
-
command: string;
|
|
864
|
-
args: string[];
|
|
865
|
-
displayCommand: string;
|
|
866
|
-
category: string;
|
|
867
|
-
source: string;
|
|
868
|
-
};
|
|
872
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
869
873
|
|
|
870
874
|
type MeshRefineValidationSummary = {
|
|
871
875
|
status: MeshRefineValidationStatus;
|
|
@@ -875,6 +879,10 @@ type MeshRefineValidationSummary = {
|
|
|
875
879
|
skippedReason?: string;
|
|
876
880
|
timeoutMs: number;
|
|
877
881
|
outputLimitBytes: number;
|
|
882
|
+
configSource?: string;
|
|
883
|
+
configSourceType?: string;
|
|
884
|
+
suggestions?: unknown[];
|
|
885
|
+
suggestedConfig?: unknown;
|
|
878
886
|
};
|
|
879
887
|
|
|
880
888
|
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
@@ -996,171 +1004,25 @@ async function runMeshRefinePatchEquivalenceGate(
|
|
|
996
1004
|
}
|
|
997
1005
|
}
|
|
998
1006
|
|
|
999
|
-
function
|
|
1000
|
-
|
|
1001
|
-
const packageJsonPath = pathJoin(workspace, 'package.json');
|
|
1002
|
-
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
1003
|
-
return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
|
|
1004
|
-
? parsed.scripts as Record<string, string>
|
|
1005
|
-
: {};
|
|
1006
|
-
} catch {
|
|
1007
|
-
return {};
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function tokenizeValidationCommand(command: string): string[] | null {
|
|
1012
|
-
const trimmed = command.trim();
|
|
1013
|
-
if (!trimmed) return null;
|
|
1014
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
1015
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
1016
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
1017
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
1018
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
1019
|
-
if (!tokens.length) return null;
|
|
1020
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
1021
|
-
return tokens;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
|
|
1025
|
-
return scriptName === category || scriptName.startsWith(`${category}:`);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function parsePackageManagerValidationCommand(
|
|
1029
|
-
rawCommand: string,
|
|
1030
|
-
category: string,
|
|
1031
|
-
scripts: Record<string, string>,
|
|
1032
|
-
source: string,
|
|
1033
|
-
): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
|
|
1034
|
-
const tokens = tokenizeValidationCommand(rawCommand);
|
|
1035
|
-
if (!tokens) {
|
|
1036
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const [binary, second, third, ...rest] = tokens;
|
|
1040
|
-
let scriptName = '';
|
|
1041
|
-
let command = binary;
|
|
1042
|
-
let args: string[] = [];
|
|
1043
|
-
|
|
1044
|
-
if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
|
|
1045
|
-
scriptName = third;
|
|
1046
|
-
args = ['run', scriptName, ...rest];
|
|
1047
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
1048
|
-
scriptName = 'test';
|
|
1049
|
-
args = ['test'];
|
|
1050
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
1051
|
-
scriptName = third;
|
|
1052
|
-
args = ['run', scriptName, ...rest];
|
|
1053
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
1054
|
-
scriptName = second;
|
|
1055
|
-
args = [scriptName];
|
|
1056
|
-
} else {
|
|
1057
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
1061
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
|
|
1062
|
-
}
|
|
1063
|
-
if (!scriptMatchesValidationCategory(scriptName, category)) {
|
|
1064
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1007
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1008
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1067
1009
|
return {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
displayCommand:
|
|
1072
|
-
category,
|
|
1073
|
-
source,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
for (const entry of entries) {
|
|
1085
|
-
if (typeof entry?.command !== 'string') continue;
|
|
1086
|
-
candidates.push({
|
|
1087
|
-
command: entry.command,
|
|
1088
|
-
category,
|
|
1089
|
-
source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
|
|
1090
|
-
confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return candidates.sort((a, b) => {
|
|
1095
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
1096
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
|
|
1101
|
-
const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
|
|
1102
|
-
const configured = Array.isArray(policy.validationCommands)
|
|
1103
|
-
? policy.validationCommands
|
|
1104
|
-
: Array.isArray(policy.validationGate?.commands)
|
|
1105
|
-
? policy.validationGate.commands
|
|
1106
|
-
: [];
|
|
1107
|
-
return configured
|
|
1108
|
-
.map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
|
|
1109
|
-
.filter((entry: any) => entry && typeof entry.command === 'string')
|
|
1110
|
-
.map((entry: any) => {
|
|
1111
|
-
const commandText = entry.command.trim();
|
|
1112
|
-
const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
|
|
1113
|
-
return { command: commandText, category, source: 'mesh.policy.validationCommands' };
|
|
1114
|
-
})
|
|
1115
|
-
.filter((entry: any) => !!entry.category);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
|
|
1119
|
-
const scripts = readPackageScripts(workspace);
|
|
1120
|
-
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
1121
|
-
const selected: MeshRefineValidationCommand[] = [];
|
|
1122
|
-
const seen = new Set<string>();
|
|
1123
|
-
const candidates = [
|
|
1124
|
-
...collectPolicyValidationCandidates(mesh),
|
|
1125
|
-
...collectProjectContextValidationCandidates(mesh),
|
|
1126
|
-
];
|
|
1127
|
-
|
|
1128
|
-
for (const candidate of candidates) {
|
|
1129
|
-
const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
|
|
1130
|
-
if (parsed.rejected) {
|
|
1131
|
-
rejectedCommands.push(parsed.rejected);
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
|
|
1135
|
-
selected.push(parsed.command);
|
|
1136
|
-
seen.add(parsed.command.displayCommand);
|
|
1137
|
-
if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (!selected.length && candidates.length === 0) {
|
|
1141
|
-
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
1142
|
-
if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
|
|
1143
|
-
const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
|
|
1144
|
-
if (fallback.command && !seen.has(fallback.command.displayCommand)) {
|
|
1145
|
-
selected.push(fallback.command);
|
|
1146
|
-
seen.add(fallback.command.displayCommand);
|
|
1147
|
-
} else if (fallback.rejected) {
|
|
1148
|
-
rejectedCommands.push(fallback.rejected);
|
|
1149
|
-
}
|
|
1150
|
-
if (selected.length >= 2) break;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
return {
|
|
1155
|
-
commands: selected,
|
|
1156
|
-
rejectedCommands,
|
|
1157
|
-
source: selected.some(command => command.source === 'mesh.policy.validationCommands')
|
|
1158
|
-
? 'mesh_policy'
|
|
1159
|
-
: selected.some(command => command.source !== 'package.json:scripts')
|
|
1160
|
-
? 'project_context'
|
|
1161
|
-
: selected.length
|
|
1162
|
-
? 'package_json_scripts'
|
|
1163
|
-
: 'unavailable',
|
|
1010
|
+
source: plan.source,
|
|
1011
|
+
sourceType: plan.sourceType,
|
|
1012
|
+
commands: plan.commands.map(command => ({
|
|
1013
|
+
displayCommand: command.displayCommand,
|
|
1014
|
+
category: command.category,
|
|
1015
|
+
source: command.source,
|
|
1016
|
+
cwd: command.cwd,
|
|
1017
|
+
timeoutMs: command.timeoutMs,
|
|
1018
|
+
})),
|
|
1019
|
+
unavailableReason: plan.unavailableReason,
|
|
1020
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1021
|
+
suggestions: plan.suggestions,
|
|
1022
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1023
|
+
note: plan.sourceType === 'unavailable'
|
|
1024
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1025
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
1164
1026
|
};
|
|
1165
1027
|
}
|
|
1166
1028
|
|
|
@@ -1168,7 +1030,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1168
1030
|
const { execFile } = await import('node:child_process');
|
|
1169
1031
|
const { promisify } = await import('node:util');
|
|
1170
1032
|
const execFileAsync = promisify(execFile);
|
|
1171
|
-
const selection =
|
|
1033
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1172
1034
|
const summary: MeshRefineValidationSummary = {
|
|
1173
1035
|
status: 'skipped',
|
|
1174
1036
|
required: true,
|
|
@@ -1177,22 +1039,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1177
1039
|
skippedReason: undefined,
|
|
1178
1040
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
1179
1041
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1042
|
+
configSource: selection.source,
|
|
1043
|
+
configSourceType: selection.sourceType,
|
|
1044
|
+
suggestions: selection.suggestions,
|
|
1045
|
+
suggestedConfig: selection.suggestedConfig,
|
|
1180
1046
|
};
|
|
1181
1047
|
|
|
1182
1048
|
if (!selection.commands.length) {
|
|
1183
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1049
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
1184
1050
|
return summary;
|
|
1185
1051
|
}
|
|
1186
1052
|
|
|
1187
1053
|
for (const candidate of selection.commands) {
|
|
1188
1054
|
const startedAt = Date.now();
|
|
1055
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1056
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1189
1057
|
try {
|
|
1190
1058
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1191
|
-
cwd
|
|
1059
|
+
cwd,
|
|
1192
1060
|
encoding: 'utf8',
|
|
1193
|
-
timeout
|
|
1061
|
+
timeout,
|
|
1194
1062
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1195
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1063
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1196
1064
|
});
|
|
1197
1065
|
summary.commandsRun.push({
|
|
1198
1066
|
command: candidate.command,
|
|
@@ -1200,6 +1068,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1200
1068
|
displayCommand: candidate.displayCommand,
|
|
1201
1069
|
category: candidate.category,
|
|
1202
1070
|
source: candidate.source,
|
|
1071
|
+
cwd,
|
|
1203
1072
|
passed: true,
|
|
1204
1073
|
exitCode: 0,
|
|
1205
1074
|
durationMs: Date.now() - startedAt,
|
|
@@ -1213,6 +1082,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1213
1082
|
displayCommand: candidate.displayCommand,
|
|
1214
1083
|
category: candidate.category,
|
|
1215
1084
|
source: candidate.source,
|
|
1085
|
+
cwd,
|
|
1216
1086
|
passed: false,
|
|
1217
1087
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1218
1088
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -1466,6 +1336,50 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
1466
1336
|
};
|
|
1467
1337
|
}
|
|
1468
1338
|
|
|
1339
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1340
|
+
const raw = hostAddress.trim();
|
|
1341
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1342
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1343
|
+
url.pathname = '/api/v1/command';
|
|
1344
|
+
url.search = '';
|
|
1345
|
+
url.hash = '';
|
|
1346
|
+
return url.toString();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1350
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1351
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1352
|
+
? args.memberNode as Record<string, any>
|
|
1353
|
+
: null;
|
|
1354
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1355
|
+
? (requestedNodeId
|
|
1356
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1357
|
+
: mesh.nodes[0])
|
|
1358
|
+
: null;
|
|
1359
|
+
const source = explicit || configured;
|
|
1360
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1361
|
+
? source.workspace.trim()
|
|
1362
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1363
|
+
? args.workspace.trim()
|
|
1364
|
+
: process.cwd();
|
|
1365
|
+
if (!workspace) return null;
|
|
1366
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1367
|
+
? source.id.trim()
|
|
1368
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1369
|
+
? source.nodeId.trim()
|
|
1370
|
+
: undefined;
|
|
1371
|
+
return {
|
|
1372
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1373
|
+
workspace,
|
|
1374
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1375
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1376
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1377
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1378
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1379
|
+
role: 'member',
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1469
1383
|
export class DaemonCommandRouter {
|
|
1470
1384
|
private deps: CommandRouterDeps;
|
|
1471
1385
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
@@ -1661,6 +1575,18 @@ export class DaemonCommandRouter {
|
|
|
1661
1575
|
this.aggregateMeshStatusCache.delete(meshId);
|
|
1662
1576
|
}
|
|
1663
1577
|
|
|
1578
|
+
|
|
1579
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1580
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1581
|
+
const mesh = meshRecord?.mesh;
|
|
1582
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1583
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1584
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1585
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1586
|
+
}
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1664
1590
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
1665
1591
|
if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
|
|
1666
1592
|
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
|
|
@@ -2876,7 +2802,10 @@ export class DaemonCommandRouter {
|
|
|
2876
2802
|
if (!name) return { success: false, error: 'name required' };
|
|
2877
2803
|
try {
|
|
2878
2804
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
2879
|
-
const
|
|
2805
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
2806
|
+
? args.meshHost
|
|
2807
|
+
: undefined;
|
|
2808
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
2880
2809
|
return { success: true, mesh };
|
|
2881
2810
|
} catch (e: any) {
|
|
2882
2811
|
return { success: false, error: e.message };
|
|
@@ -2893,6 +2822,7 @@ export class DaemonCommandRouter {
|
|
|
2893
2822
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2894
2823
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2895
2824
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
2825
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2896
2826
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2897
2827
|
const mesh = updateMesh(meshId, patch as any);
|
|
2898
2828
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
@@ -2904,6 +2834,225 @@ export class DaemonCommandRouter {
|
|
|
2904
2834
|
}
|
|
2905
2835
|
}
|
|
2906
2836
|
|
|
2837
|
+
case 'get_mesh_host_pairing': {
|
|
2838
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2839
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2840
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2841
|
+
const mesh = meshRecord?.mesh;
|
|
2842
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2843
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2844
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
2845
|
+
return {
|
|
2846
|
+
success: true,
|
|
2847
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
2848
|
+
meshId,
|
|
2849
|
+
hostAddress: meshHost.hostAddress,
|
|
2850
|
+
meshHost,
|
|
2851
|
+
manualPairing: {
|
|
2852
|
+
status: pairingStatus,
|
|
2853
|
+
joinImplemented: true,
|
|
2854
|
+
protocol: 'standalone_command_direct_v1',
|
|
2855
|
+
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
2856
|
+
},
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
case 'configure_mesh_host_pairing': {
|
|
2861
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2862
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
2863
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2864
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2865
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
2866
|
+
try {
|
|
2867
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
2868
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
2869
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
2870
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
2871
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
2872
|
+
return {
|
|
2873
|
+
success: true,
|
|
2874
|
+
code: 'mesh_host_pairing_pending',
|
|
2875
|
+
meshId,
|
|
2876
|
+
hostAddress: configured.hostAddress,
|
|
2877
|
+
meshHost,
|
|
2878
|
+
manualPairing: {
|
|
2879
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
2880
|
+
joinImplemented: true,
|
|
2881
|
+
protocol: 'standalone_command_direct_v1',
|
|
2882
|
+
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
2883
|
+
},
|
|
2884
|
+
};
|
|
2885
|
+
} catch (e: any) {
|
|
2886
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
case 'create_mesh_host_pairing_token': {
|
|
2891
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2892
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2893
|
+
try {
|
|
2894
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
2895
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
2896
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
2897
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
2898
|
+
});
|
|
2899
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
2900
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
2901
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2902
|
+
return {
|
|
2903
|
+
success: true,
|
|
2904
|
+
code: 'mesh_host_pairing_token_created',
|
|
2905
|
+
meshId,
|
|
2906
|
+
token: created.token,
|
|
2907
|
+
tokenId: created.tokenId,
|
|
2908
|
+
expiresAt: created.expiresAt,
|
|
2909
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
2910
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
2911
|
+
};
|
|
2912
|
+
} catch (e: any) {
|
|
2913
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
case 'apply_mesh_host_join': {
|
|
2918
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2919
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2920
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2921
|
+
? args.memberNode
|
|
2922
|
+
: null;
|
|
2923
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2924
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
2925
|
+
try {
|
|
2926
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
2927
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
2928
|
+
token,
|
|
2929
|
+
memberNode: memberNode as any,
|
|
2930
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
2931
|
+
});
|
|
2932
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
2933
|
+
if (!applied.accepted) {
|
|
2934
|
+
return {
|
|
2935
|
+
success: false,
|
|
2936
|
+
code: 'mesh_host_join_rejected',
|
|
2937
|
+
meshId,
|
|
2938
|
+
tokenId: applied.tokenId,
|
|
2939
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
2940
|
+
error: applied.reason,
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
2944
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2945
|
+
try {
|
|
2946
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2947
|
+
appendLedgerEntry(meshId, {
|
|
2948
|
+
kind: 'node_joined',
|
|
2949
|
+
nodeId: applied.node.id,
|
|
2950
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
2951
|
+
});
|
|
2952
|
+
} catch { /* ledger append is best-effort */ }
|
|
2953
|
+
return {
|
|
2954
|
+
success: true,
|
|
2955
|
+
code: 'mesh_host_join_accepted',
|
|
2956
|
+
meshId,
|
|
2957
|
+
node: applied.node,
|
|
2958
|
+
tokenId: applied.tokenId,
|
|
2959
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
2960
|
+
};
|
|
2961
|
+
} catch (e: any) {
|
|
2962
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
case 'join_mesh_host_pairing': {
|
|
2967
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2968
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2969
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2970
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
2971
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2972
|
+
const mesh = meshRecord?.mesh;
|
|
2973
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2974
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2975
|
+
if (meshHost.role !== 'member') {
|
|
2976
|
+
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
2977
|
+
}
|
|
2978
|
+
try {
|
|
2979
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
2980
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
2981
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
2982
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
2983
|
+
}
|
|
2984
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
2985
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
2986
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
2987
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
2988
|
+
? args.hostDaemonId.trim()
|
|
2989
|
+
: meshHost.hostDaemonId;
|
|
2990
|
+
let hostResult: any;
|
|
2991
|
+
let transport: string;
|
|
2992
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
2993
|
+
transport = 'mesh_command_dispatch';
|
|
2994
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
2995
|
+
meshId: hostMeshId,
|
|
2996
|
+
token,
|
|
2997
|
+
memberMeshId: meshId,
|
|
2998
|
+
memberNode,
|
|
2999
|
+
});
|
|
3000
|
+
} else if (meshHost.hostAddress) {
|
|
3001
|
+
transport = 'standalone_http_command';
|
|
3002
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3003
|
+
const response = await fetch(commandUrl, {
|
|
3004
|
+
method: 'POST',
|
|
3005
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3006
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3007
|
+
});
|
|
3008
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3009
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3010
|
+
} else {
|
|
3011
|
+
return {
|
|
3012
|
+
success: false,
|
|
3013
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3014
|
+
meshId,
|
|
3015
|
+
meshHost,
|
|
3016
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
if (!hostResult?.success) {
|
|
3020
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3021
|
+
}
|
|
3022
|
+
const joined = meshRecord.inline
|
|
3023
|
+
? null
|
|
3024
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3025
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3026
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3027
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3028
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3029
|
+
});
|
|
3030
|
+
if (joined) {
|
|
3031
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3032
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3033
|
+
}
|
|
3034
|
+
return {
|
|
3035
|
+
success: true,
|
|
3036
|
+
code: 'mesh_host_join_applied',
|
|
3037
|
+
meshId,
|
|
3038
|
+
hostMeshId,
|
|
3039
|
+
transport,
|
|
3040
|
+
node: hostResult.node,
|
|
3041
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3042
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3043
|
+
hostResult,
|
|
3044
|
+
manualPairing: {
|
|
3045
|
+
status: 'paired',
|
|
3046
|
+
joinImplemented: true,
|
|
3047
|
+
protocol: 'standalone_command_direct_v1',
|
|
3048
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3049
|
+
},
|
|
3050
|
+
};
|
|
3051
|
+
} catch (e: any) {
|
|
3052
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2907
3056
|
case 'delete_mesh': {
|
|
2908
3057
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2909
3058
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2997,6 +3146,8 @@ export class DaemonCommandRouter {
|
|
|
2997
3146
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2998
3147
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2999
3148
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3149
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3150
|
+
if (ownerFailure) return ownerFailure;
|
|
3000
3151
|
try {
|
|
3001
3152
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
3002
3153
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -3012,6 +3163,8 @@ export class DaemonCommandRouter {
|
|
|
3012
3163
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3013
3164
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
3014
3165
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3166
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
3167
|
+
if (ownerFailure) return ownerFailure;
|
|
3015
3168
|
try {
|
|
3016
3169
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
3017
3170
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -3033,6 +3186,8 @@ export class DaemonCommandRouter {
|
|
|
3033
3186
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
3034
3187
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3035
3188
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
3189
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
3190
|
+
if (ownerFailure) return ownerFailure;
|
|
3036
3191
|
try {
|
|
3037
3192
|
const { addNode } = await import('../config/mesh-config.js');
|
|
3038
3193
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -3043,7 +3198,8 @@ export class DaemonCommandRouter {
|
|
|
3043
3198
|
...(readOnly ? { readOnly: true } : {}),
|
|
3044
3199
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
3045
3200
|
};
|
|
3046
|
-
const
|
|
3201
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
3202
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}), ...(role ? { role } : {}) });
|
|
3047
3203
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
3048
3204
|
return { success: true, node };
|
|
3049
3205
|
} catch (e: any) {
|
|
@@ -3055,6 +3211,8 @@ export class DaemonCommandRouter {
|
|
|
3055
3211
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3056
3212
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3057
3213
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3214
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
3215
|
+
if (ownerFailure) return ownerFailure;
|
|
3058
3216
|
try {
|
|
3059
3217
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
3060
3218
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -3083,6 +3241,8 @@ export class DaemonCommandRouter {
|
|
|
3083
3241
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3084
3242
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3085
3243
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3244
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
3245
|
+
if (ownerFailure) return ownerFailure;
|
|
3086
3246
|
try {
|
|
3087
3247
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3088
3248
|
const mesh = meshRecord?.mesh;
|
|
@@ -3108,6 +3268,57 @@ export class DaemonCommandRouter {
|
|
|
3108
3268
|
}
|
|
3109
3269
|
}
|
|
3110
3270
|
|
|
3271
|
+
case 'get_mesh_refine_config_schema': {
|
|
3272
|
+
return {
|
|
3273
|
+
success: true,
|
|
3274
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
3275
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
3276
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
3277
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
case 'validate_mesh_refine_config': {
|
|
3282
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3283
|
+
const mesh = args?.inlineMesh || {};
|
|
3284
|
+
const loaded = args?.config !== undefined
|
|
3285
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
3286
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
3287
|
+
const validation = loaded.config
|
|
3288
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
3289
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
3290
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
case 'suggest_mesh_refine_config': {
|
|
3294
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3295
|
+
const mesh = args?.inlineMesh || {};
|
|
3296
|
+
return {
|
|
3297
|
+
success: true,
|
|
3298
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
3299
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
case 'plan_mesh_refine_node': {
|
|
3304
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3305
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3306
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3307
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3308
|
+
const mesh = meshRecord?.mesh;
|
|
3309
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3310
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
3311
|
+
return {
|
|
3312
|
+
success: true,
|
|
3313
|
+
dryRun: true,
|
|
3314
|
+
nodeId,
|
|
3315
|
+
workspace: node.workspace,
|
|
3316
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
3317
|
+
mergeWillRun: false,
|
|
3318
|
+
cleanupWillRun: false,
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3111
3322
|
case 'refine_mesh_node': {
|
|
3112
3323
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3113
3324
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
@@ -3415,6 +3626,8 @@ export class DaemonCommandRouter {
|
|
|
3415
3626
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3416
3627
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
3417
3628
|
if (!branch) return { success: false, error: 'branch required' };
|
|
3629
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
3630
|
+
if (ownerFailure) return ownerFailure;
|
|
3418
3631
|
|
|
3419
3632
|
try {
|
|
3420
3633
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -3505,6 +3718,8 @@ export class DaemonCommandRouter {
|
|
|
3505
3718
|
case 'trigger_mesh_queue': {
|
|
3506
3719
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3507
3720
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3721
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
3722
|
+
if (ownerFailure) return ownerFailure;
|
|
3508
3723
|
try {
|
|
3509
3724
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
3510
3725
|
if (meshId) {
|
|
@@ -3536,6 +3751,15 @@ export class DaemonCommandRouter {
|
|
|
3536
3751
|
mesh = getMesh(meshId);
|
|
3537
3752
|
}
|
|
3538
3753
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3754
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3755
|
+
if (!meshHost.canOwnCoordinator) {
|
|
3756
|
+
return {
|
|
3757
|
+
success: false,
|
|
3758
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
3759
|
+
meshId,
|
|
3760
|
+
cliType,
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3539
3763
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
3540
3764
|
|
|
3541
3765
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -3909,6 +4133,7 @@ export class DaemonCommandRouter {
|
|
|
3909
4133
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3910
4134
|
const mesh = meshRecord?.mesh;
|
|
3911
4135
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4136
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3912
4137
|
|
|
3913
4138
|
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
3914
4139
|
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
@@ -4030,6 +4255,7 @@ export class DaemonCommandRouter {
|
|
|
4030
4255
|
repoRoot: node.repoRoot,
|
|
4031
4256
|
isLocalWorktree: node.isLocalWorktree,
|
|
4032
4257
|
worktreeBranch: node.worktreeBranch,
|
|
4258
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
4033
4259
|
daemonId,
|
|
4034
4260
|
machineId: node.machineId,
|
|
4035
4261
|
machineStatus: node.machineStatus,
|
|
@@ -4230,6 +4456,7 @@ export class DaemonCommandRouter {
|
|
|
4230
4456
|
repoIdentity: mesh.repoIdentity,
|
|
4231
4457
|
defaultBranch: mesh.defaultBranch,
|
|
4232
4458
|
refreshedAt,
|
|
4459
|
+
meshHost,
|
|
4233
4460
|
sourceOfTruth: {
|
|
4234
4461
|
membership: meshRecord?.source === 'inline_cache'
|
|
4235
4462
|
? 'coordinator_inline_mesh_cache'
|
|
@@ -4237,6 +4464,13 @@ export class DaemonCommandRouter {
|
|
|
4237
4464
|
? 'local_mesh_config'
|
|
4238
4465
|
: 'inline_bootstrap_snapshot',
|
|
4239
4466
|
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4467
|
+
meshHost: {
|
|
4468
|
+
owner: 'mesh_host_daemon',
|
|
4469
|
+
localRole: meshHost.role,
|
|
4470
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
4471
|
+
hostNodeId: meshHost.hostNodeId,
|
|
4472
|
+
hostAddress: meshHost.hostAddress,
|
|
4473
|
+
},
|
|
4240
4474
|
...(requireDirectPeerTruth ? {
|
|
4241
4475
|
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
4242
4476
|
directPeerTruth: {
|